Enlaces a las aplicaciones html
Ordenamos las frases.
https://disanedu.linexedu.eu/aplicaciones/inteligencia/lenguaje/ordenamos-frases.html (elaborado con Canvas de Gemini)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/lenguaje/ordenar-frases.html (elaborado con Claude)
Ordenamos las palabras.
https://disanedu.linexedu.eu/aplicaciones/inteligencia/lenguaje/ordenamos-palabras.html (elaborado con Canvas de Gemini)
https://disanedu.linexedu.eu/aplicaciones/inteligencia/lenguaje/ordenar-palabras.html (elaborado con Claude)
Aplicaciones html en un Site
Código html Ordenamos frases (Gemini)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>¡Ordenamos las frases!</title>
<style>
:root {
/* Paleta de colores accesible y amigable */
--primary: #0056b3;
--primary-hover: #004494;
--secondary: #e0f2fe;
--bg-color: #f8fafc;
--text-color: #1e293b;
--text-light: #475569;
--success: #16a34a;
--warning: #ea580c;
--card-bg: #ffffff;
--border-color: #cbd5e1;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius: 12px;
--font-main: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* Accesibilidad: Focus visible */
:focus-visible {
outline: 4px solid var(--primary);
outline-offset: 2px;
border-radius: 4px;
}
/* Pantallas de la aplicación */
.screen {
display: none;
flex: 1;
flex-direction: column;
padding: 1rem;
max-width: 1000px;
margin: 0 auto;
width: 100%;
}
.screen.active {
display: flex;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
font-size: 1.1rem;
font-weight: bold;
}
.icon-btn {
background: none;
border: 2px solid transparent;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover { background-color: var(--secondary); }
.btn {
background-color: var(--primary);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1.25rem;
font-weight: bold;
border-radius: 50px;
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-decoration: none;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.btn:disabled {
background-color: var(--border-color);
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background-color: white;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-secondary:hover { background-color: var(--secondary); }
/* Nuevo botón de volver atrás, más atractivo */
.btn-back {
background-color: white;
color: var(--primary);
border: 2px solid var(--border-color);
padding: 0.6rem 1.2rem;
border-radius: 50px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
box-shadow: var(--shadow);
text-decoration: none;
}
.btn-back:hover {
border-color: var(--primary);
background-color: var(--secondary);
transform: translateX(-4px); /* Efecto de desplazamiento a la izquierda */
}
.btn-back:focus-visible {
outline: 4px solid var(--primary);
}
.instruction {
text-align: center;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1.5rem;
color: var(--primary);
}
.zone {
min-height: 120px;
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.target-zone {
background-color: white;
border: 3px dashed var(--border-color);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
transition: border-color 0.3s;
}
.target-zone.drag-over {
border-color: var(--primary);
background-color: var(--secondary);
}
.source-zone {
background-color: var(--secondary);
border: 2px solid transparent;
}
.word {
background-color: white;
border: 2px solid var(--border-color);
border-bottom-width: 4px;
padding: 0.75rem 1.5rem;
font-size: 1.5rem;
font-weight: bold;
border-radius: 8px;
cursor: grab;
user-select: none;
touch-action: none; /* Previene scroll al arrastrar en táctil */
box-shadow: var(--shadow);
transition: transform 0.1s, box-shadow 0.1s;
display: flex;
align-items: center;
justify-content: center;
}
.word:active {
cursor: grabbing;
}
.word.dragging {
opacity: 0.5;
transform: scale(1.05);
box-shadow: var(--shadow-hover);
}
.word:focus-visible {
border-color: var(--primary);
}
.word.in-target {
border-color: var(--primary);
border-bottom-color: var(--primary-hover);
}
/* Espacio reservado visualmente al arrastrar */
.placeholder {
width: 100px; /* Se ajustará dinámicamente */
height: 3rem;
border: 2px dashed var(--primary);
border-radius: 8px;
background-color: rgba(0, 86, 179, 0.1);
pointer-events: none;
}
.menu-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
gap: 2rem;
}
.title {
font-size: 3rem;
color: var(--primary);
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.25rem;
color: var(--text-light);
max-width: 600px;
}
.menu-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 400px;
}
.feedback-message {
text-align: center;
font-size: 1.5rem;
font-weight: bold;
min-height: 2.5rem;
margin-bottom: 1rem;
border-radius: var(--radius);
padding: 0.5rem;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(-10px);
}
.feedback-message.show {
opacity: 1;
transform: translateY(0);
}
.feedback-success { color: var(--success); background-color: #dcfce7; }
.feedback-warning { color: var(--warning); background-color: #ffedd5; }
.feedback-info { color: var(--primary); background-color: var(--secondary); }
.custom-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
background: white;
padding: 2rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.input-text {
width: 100%;
padding: 1rem;
font-size: 1.25rem;
border: 2px solid var(--border-color);
border-radius: 8px;
font-family: inherit;
}
.input-text:focus {
border-color: var(--primary);
outline: none;
}
.results-container {
background: white;
padding: 3rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
text-align: center;
max-width: 600px;
margin: auto;
width: 100%;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 1.25rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.stat-row:last-child { border: none; }
/* Animaciones */
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.animate-pop { animation: pop 0.5s ease; }
/* Contenedor de animaciones (confeti/estrellas) */
#animation-layer {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 1000;
overflow: hidden;
}
.particle {
position: absolute;
width: 15px; height: 15px;
background-color: gold;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
animation: fall linear forwards;
}
@keyframes fall {
to { transform: translateY(100vh) rotate(360deg); opacity: 0; }
}
/* Responsividad */
@media (max-width: 600px) {
.title { font-size: 2.2rem; }
.word { font-size: 1.2rem; padding: 0.5rem 1rem; }
.btn { font-size: 1.1rem; width: 100%; }
.header-stats { flex-direction: column; gap: 0.2rem; text-align: center; }
header { flex-direction: column; gap: 1rem; }
.controls-row { display: flex; width: 100%; justify-content: space-between; align-items: center;}
}
.hidden { display: none !important; }
.solution-word { background-color: #dcfce7; border-color: var(--success); color: #14532d; }
/* Estilos de la ventana modal personalizada */
.modal-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
backdrop-filter: blur(3px);
}
.modal-content {
background-color: white;
padding: 2.5rem;
border-radius: var(--radius);
box-shadow: var(--shadow-hover);
max-width: 90%;
width: 450px;
text-align: center;
animation: pop 0.3s ease;
}
.modal-content h2 {
color: var(--primary);
margin-bottom: 1rem;
font-size: 1.8rem;
}
.modal-content p {
color: var(--text-light);
font-size: 1.2rem;
margin-bottom: 2rem;
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 1rem;
}
@media (max-width: 600px) {
.modal-buttons { flex-direction: column; }
}
</style>
</head>
<body>
<!-- Capa para animaciones -->
<div id="animation-layer" aria-hidden="true"></div>
<!-- Región ARIA para mensajes de lectores de pantalla -->
<div id="aria-live-region" class="sr-only" aria-live="polite" style="position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;"></div>
<!-- Modal de Confirmación para salir -->
<div id="exit-modal" class="modal-overlay hidden" aria-modal="true" role="dialog">
<div class="modal-content">
<h2>¿Volver al menú?</h2>
<p>Se perderá el progreso de esta partida. ¿Estás seguro?</p>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="app.closeExitModal()">Cancelar</button>
<button class="btn" onclick="app.confirmExitAction()">Sí, salir al menú</button>
</div>
</div>
</div>
<main id="app">
<!-- PANTALLA 1: MENÚ -->
<section id="screen-menu" class="screen active">
<div class="menu-container">
<div>
<h1 class="title">¡Ordenamos las frases!</h1>
<p class="subtitle">Construye cada frase colocando las palabras en el orden correcto. Toca o arrastra las palabras.</p>
</div>
<div class="menu-buttons">
<button class="btn" onclick="app.startGame(false)" aria-label="Jugar modo ordenar frases">
🧩 Ordenar frases propuestas
</button>
<button class="btn btn-secondary" onclick="app.showScreen('screen-custom')" aria-label="Crear mi propia frase">
✏️ Crear mi propia frase
</button>
</div>
<button id="btn-sound-menu" class="icon-btn" onclick="app.toggleSound()" aria-label="Activar o desactivar sonido" title="Sonido">
🔊
</button>
</div>
</section>
<!-- PANTALLA 2: CREAR FRASE -->
<section id="screen-custom" class="screen">
<header>
<button class="btn-back" onclick="app.showScreen('screen-menu')" aria-label="Volver al menú principal" title="Volver">
🏠 Menú
</button>
<h2>Modo personalizado</h2>
<div style="width: 110px;"></div> <!-- Espaciador para equilibrar el header -->
</header>
<div class="custom-form">
<div style="text-align: left;">
<label style="font-size: 1.25rem; font-weight: bold; color: var(--primary);">Escribe entre 1 y 5 frases para ordenar:</label>
<p style="color: var(--text-light); margin-top: 0.25rem;">Rellena solo las que necesites (las casillas vacías no se usarán).</p>
</div>
<input type="text" id="custom-input-1" class="input-text" placeholder="Frase 1: El perro juega en el parque." autocomplete="off">
<input type="text" id="custom-input-2" class="input-text" placeholder="Frase 2 (opcional)" autocomplete="off">
<input type="text" id="custom-input-3" class="input-text" placeholder="Frase 3 (opcional)" autocomplete="off">
<input type="text" id="custom-input-4" class="input-text" placeholder="Frase 4 (opcional)" autocomplete="off">
<input type="text" id="custom-input-5" class="input-text" placeholder="Frase 5 (opcional)" autocomplete="off">
<div id="custom-error" style="color: var(--warning); display: none; background-color: #ffedd5; padding: 1rem; border-radius: 8px; font-weight: bold; text-align: center;">
Por favor, escribe al menos una frase válida (debe tener 2 o más palabras).
</div>
<button class="btn" onclick="app.prepareCustomSentence()" style="margin-top: 1rem;">▶️ ¡A jugar!</button>
</div>
</section>
<!-- PANTALLA 3: JUEGO -->
<section id="screen-game" class="screen">
<header>
<div class="controls-row">
<button class="btn-back" onclick="app.confirmExit()" aria-label="Volver al menú principal" title="Volver">
🏠 Menú
</button>
<button id="btn-sound-game" class="icon-btn" onclick="app.toggleSound()" aria-label="Activar o desactivar sonido" title="Sonido">🔊</button>
</div>
<div class="header-stats">
<span id="progress-text" aria-live="polite">Frase 1 de 5</span>
<span id="attempts-text" aria-live="polite">Intentos: 3</span>
</div>
</header>
<h2 class="instruction" id="game-instruction">Ordena las palabras para formar la frase.</h2>
<div id="feedback-msg" class="feedback-message" role="status"></div>
<!-- Zona donde se construye la frase -->
<div id="target-zone" class="zone target-zone" aria-label="Coloca aquí las palabras" role="list">
<!-- Las palabras se insertarán aquí -->
</div>
<!-- Zona de palabras disponibles -->
<div id="source-zone" class="zone source-zone" aria-label="Palabras disponibles" role="list">
<!-- Las palabras se insertarán aquí -->
</div>
<div style="text-align: center; margin-top: 1rem;">
<button id="btn-check" class="btn" onclick="app.checkAnswer()" style="display: none; margin: 0 auto;">Comprobar</button>
<button id="btn-next" class="btn" onclick="app.nextSentence()" style="display: none; margin: 0 auto;">Siguiente frase ➡️</button>
</div>
</section>
<!-- PANTALLA 4: RESULTADOS -->
<section id="screen-results" class="screen">
<div class="results-container">
<h2 class="title" style="font-size: 2.5rem;">¡Partida terminada!</h2>
<p id="result-motivational" class="subtitle" style="margin: 1.5rem auto; font-weight: bold; color: var(--primary);"></p>
<div style="margin-bottom: 2rem; text-align: left;">
<div class="stat-row">
<span>✅ Acertadas a la primera:</span>
<span id="stat-first" style="font-weight: bold;">0</span>
</div>
<div class="stat-row">
<span>🔄 Acertadas con varios intentos:</span>
<span id="stat-help" style="font-weight: bold;">0</span>
</div>
<div class="stat-row">
<span>💡 Necesitaron ver la solución:</span>
<span id="stat-failed" style="font-weight: bold;">0</span>
</div>
</div>
<div class="menu-buttons" style="margin: 0 auto;">
<button class="btn" onclick="app.startGame(false)">Jugar de nuevo</button>
<button class="btn btn-secondary" onclick="app.showScreen('screen-menu')">Volver al menú</button>
</div>
</div>
</section>
</main>
<script>
/**
* Datos de la aplicación
*/
const SENTENCE_BANK = [
"Mamá hizo zumo de naranja.", "Quiero mucho a mi tía.", "Papá se fue a trabajar.",
"Elena es muy buena chica.", "La leche es muy sana.", "Me gusta el agua fresca.",
"Clara es amiga de Ana.", "Mi abuelo vive en Madrid.", "Pedro cuida de su hermano.",
"El soldadito perdió su gorro.", "Tengo ojos en la cara.", "El abuelo Paco fuma tabaco.",
"El pastel era de crema.", "La bruja rompió su escoba.", "Comí un flan de huevo.",
"La tarta es de chocolate.", "La primavera la sangre altera.", "Jaime me regaló un caballo.",
"El dragón no arrojó fuego.", "Alberto se comió un melocotón.", "El elefante juega al tenis.",
"Los delfines están muy contentos.", "Diego compró una moto roja.", "El sastre hizo un traje.",
"El agua es muy sana.", "María participó en la carrera.", "El gato marrón era peludo.",
"El tren llegó tarde.", "Blancanieves se comió la manzana.", "Miguel tiene una bicicleta nueva."
];
const MAX_ATTEMPTS = 3;
const SENTENCES_PER_GAME = 5;
const MOTIVATIONAL_MSGS = [
"¡Muy bien!", "¡Excelente trabajo!", "¡Lo has conseguido!",
"¡Genial!", "¡Qué bien has ordenado la frase!", "¡Sigue así!"
];
/* Sistema de Sonido Simple (Web Audio API) para no depender de archivos externos */
const AudioSys = {
ctx: null,
init: function() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if(AudioContext) this.ctx = new AudioContext();
}
},
playTone: function(freq, type, duration, vol) {
if (!app.state.soundEnabled || !this.ctx) return;
if (this.ctx.state === 'suspended') this.ctx.resume();
try {
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);
} catch(e) { console.warn("Audio error", e); }
},
playSuccess: function() {
this.init();
this.playTone(523.25, 'sine', 0.15, 0.3); // C5
setTimeout(() => this.playTone(659.25, 'sine', 0.3, 0.3), 150); // E5
},
playSoftError: function() {
this.init();
this.playTone(300, 'triangle', 0.2, 0.2);
},
playWinGame: function() {
this.init();
[523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 'sine', 0.2, 0.3), i * 150);
});
}
};
const app = {
state: {
isCustomMode: false,
soundEnabled: true,
gameSentences: [],
currentSentenceIndex: 0,
currentWordsObj: [], // Array of objects {id, text}
attemptsLeft: 3,
stats: { firstTry: 0, withHelp: 0, failed: 0 }
},
elements: {
targetZone: document.getElementById('target-zone'),
sourceZone: document.getElementById('source-zone'),
btnCheck: document.getElementById('btn-check'),
btnNext: document.getElementById('btn-next'),
feedbackMsg: document.getElementById('feedback-msg'),
ariaLive: document.getElementById('aria-live-region')
},
// --- Utilidades ---
shuffleArray: function(array) {
let curId = array.length;
while (0 !== curId) {
let randId = Math.floor(Math.random() * curId);
curId -= 1;
let tmp = array[curId];
array[curId] = array[randId];
array[randId] = tmp;
}
return array;
},
speak: function(text) {
if(!this.state.soundEnabled || !window.speechSynthesis) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'es-ES';
utterance.rate = 0.9; // Habla ligeramente más despacio
window.speechSynthesis.speak(utterance);
},
announce: function(message) {
this.elements.ariaLive.textContent = message;
},
toggleSound: function() {
this.state.soundEnabled = !this.state.soundEnabled;
const icon = this.state.soundEnabled ? '🔊' : '🔇';
document.getElementById('btn-sound-menu').textContent = icon;
document.getElementById('btn-sound-game').textContent = icon;
if(this.state.soundEnabled) AudioSys.init();
},
showScreen: function(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
window.scrollTo(0,0);
},
showFeedback: function(msg, type) {
const fb = this.elements.feedbackMsg;
fb.textContent = msg;
fb.className = `feedback-message show feedback-${type}`;
this.announce(msg);
},
hideFeedback: function() {
this.elements.feedbackMsg.classList.remove('show');
},
startGame: function(isCustom, customSentencesArray = []) {
this.state.isCustomMode = isCustom;
this.state.stats = { firstTry: 0, withHelp: 0, failed: 0 };
if (isCustom && customSentencesArray.length > 0) {
this.state.gameSentences = customSentencesArray;
} else {
// Seleccionar 5 frases aleatorias sin repetir
let shuffledBank = this.shuffleArray([...SENTENCE_BANK]);
this.state.gameSentences = shuffledBank.slice(0, SENTENCES_PER_GAME);
}
this.state.currentSentenceIndex = 0;
this.showScreen('screen-game');
if(this.state.soundEnabled) AudioSys.init();
this.loadSentence();
},
prepareCustomSentence: function() {
const error = document.getElementById('custom-error');
let validSentences = [];
// Recorrer los 5 campos de texto
for (let i = 1; i <= 5; i++) {
const input = document.getElementById(`custom-input-${i}`);
if (input) {
const text = input.value.trim();
// Validación básica: al menos 2 palabras
if (text.split(/\s+/).length >= 2) {
validSentences.push(text);
}
}
}
if (validSentences.length === 0) {
error.style.display = 'block';
// Hacer foco en el primer input si falla
document.getElementById('custom-input-1').focus();
return;
}
error.style.display = 'none';
// Limpiar inputs para la próxima vez
for (let i = 1; i <= 5; i++) {
const input = document.getElementById(`custom-input-${i}`);
if (input) input.value = '';
}
this.startGame(true, validSentences);
},
loadSentence: function() {
this.state.attemptsLeft = MAX_ATTEMPTS;
this.hideFeedback();
this.elements.btnNext.style.display = 'none';
this.elements.btnCheck.style.display = 'none';
this.elements.targetZone.innerHTML = '';
this.elements.sourceZone.innerHTML = '';
this.elements.targetZone.classList.remove('solution-word'); // Reset visual
this.updateStatusBar();
const sentence = this.state.gameSentences[this.state.currentSentenceIndex];
// Dividir manteniendo puntuación pegada a la palabra.
// Regex simple por espacios funciona bien para el propósito escolar.
const words = sentence.trim().split(/\s+/);
// Crear objetos palabra para tener IDs únicos
this.state.currentWordsObj = words.map((w, i) => ({ id: `w-${i}`, text: w, originalIndex: i }));
// Barajar asegurando que no quede igual a la original (si es posible)
let scrambled = [...this.state.currentWordsObj];
let isSame = true;
let tries = 0;
while (isSame && tries < 5 && scrambled.length > 1) {
scrambled = this.shuffleArray(scrambled);
isSame = scrambled.every((w, i) => w.id === this.state.currentWordsObj[i].id);
tries++;
}
// Renderizar
scrambled.forEach(wordObj => {
const el = this.createWordElement(wordObj);
this.elements.sourceZone.appendChild(el);
});
this.announce(`Nueva frase. Tienes ${this.state.currentWordsObj.length} palabras para ordenar.`);
this.checkAutoCheck(); // Por si hay algún estado raro
},
updateStatusBar: function() {
const total = this.state.gameSentences.length;
const current = this.state.currentSentenceIndex + 1;
document.getElementById('progress-text').textContent = `Frase ${current} de ${total}`;
document.getElementById('attempts-text').textContent = `Oportunidades: ${this.state.attemptsLeft}`;
},
createWordElement: function(wordObj) {
const div = document.createElement('div');
div.className = 'word';
div.textContent = wordObj.text;
div.dataset.id = wordObj.id;
div.tabIndex = 0;
div.setAttribute('role', 'button');
div.setAttribute('aria-pressed', 'false');
// Interacción accesible (Clic / Tap)
div.addEventListener('click', (e) => {
this.handleWordClick(div);
});
div.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleWordClick(div);
}
});
// Implementación Drag & Drop robusta (Pointer Events para ratón/táctil unificado)
div.addEventListener('pointerdown', this.handlePointerDown.bind(this));
return div;
},
handleWordClick: function(wordEl) {
// Si la palabra está en origen, va al destino. Si está en destino, vuelve al origen.
if (wordEl.parentElement === this.elements.sourceZone) {
this.elements.targetZone.appendChild(wordEl);
wordEl.classList.add('in-target');
AudioSys.init(); // Preparar audio
} else {
this.elements.sourceZone.appendChild(wordEl);
wordEl.classList.remove('in-target');
}
this.checkAutoCheck();
},
// --- Lógica Drag & Drop personalizada (Pointer Events) ---
dragState: { element: null, clone: null, placeholder: null, startX: 0, startY: 0, offsetX: 0, offsetY: 0 },
handlePointerDown: function(e) {
// Solo botón izquierdo o táctil primario
if (e.button !== 0 && e.pointerType === 'mouse') return;
const target = e.target.closest('.word');
if (!target) return;
e.preventDefault(); // Previene comportamientos por defecto (ej. scroll táctil)
target.setPointerCapture(e.pointerId);
this.dragState.element = target;
// Crear clon para mover visualmente
const rect = target.getBoundingClientRect();
this.dragState.offsetX = e.clientX - rect.left;
this.dragState.offsetY = e.clientY - rect.top;
const clone = target.cloneNode(true);
clone.classList.add('dragging');
clone.style.position = 'fixed';
clone.style.left = rect.left + 'px';
clone.style.top = rect.top + 'px';
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
clone.style.zIndex = '1000';
clone.style.pointerEvents = 'none'; // Para que mousemove detecte elementos debajo
document.body.appendChild(clone);
this.dragState.clone = clone;
// Crear placeholder para el hueco
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
placeholder.style.width = rect.width + 'px';
placeholder.style.height = rect.height + 'px';
this.dragState.placeholder = placeholder;
// Insertar placeholder donde estaba la palabra e invisibilizar original
target.parentNode.insertBefore(placeholder, target);
target.style.display = 'none';
// Añadir listeners para movimiento y fin
target.addEventListener('pointermove', this.handlePointerMove.bind(this));
target.addEventListener('pointerup', this.handlePointerUp.bind(this));
target.addEventListener('pointercancel', this.handlePointerUp.bind(this));
},
handlePointerMove: function(e) {
if (!this.dragState.clone) return;
// Mover clon
const x = e.clientX - this.dragState.offsetX;
const y = e.clientY - this.dragState.offsetY;
this.dragState.clone.style.left = x + 'px';
this.dragState.clone.style.top = y + 'px';
// Determinar zona de dropeo
// Esconder clon momentáneamente para buscar debajo
this.dragState.clone.style.display = 'none';
const elemBelow = document.elementFromPoint(e.clientX, e.clientY);
this.dragState.clone.style.display = 'flex';
if (!elemBelow) return;
const dropZone = elemBelow.closest('.zone');
// Estilos visuales de zona
document.querySelectorAll('.zone').forEach(z => z.classList.remove('drag-over'));
if (dropZone) {
dropZone.classList.add('drag-over');
// Lógica para ordenar dentro de la zona
const siblings = [...dropZone.querySelectorAll('.word:not([style*="display: none"]), .placeholder')];
// Encontrar el siguiente hermano basado en la posición del ratón
const nextSibling = siblings.find(sibling => {
const box = sibling.getBoundingClientRect();
// Asumimos layout horizontal/flex. Comparamos centro del elemento
return e.clientX < box.left + box.width / 2;
});
if (nextSibling !== this.dragState.placeholder) {
dropZone.insertBefore(this.dragState.placeholder, nextSibling);
}
}
},
handlePointerUp: function(e) {
if (!this.dragState.element) return;
const target = this.dragState.element;
target.releasePointerCapture(e.pointerId);
target.removeEventListener('pointermove', this.handlePointerMove);
target.removeEventListener('pointerup', this.handlePointerUp);
target.removeEventListener('pointercancel', this.handlePointerUp);
// Limpiar estilos de zona
document.querySelectorAll('.zone').forEach(z => z.classList.remove('drag-over'));
// Restaurar original en la posición del placeholder
if (this.dragState.placeholder.parentNode) {
this.dragState.placeholder.parentNode.insertBefore(target, this.dragState.placeholder);
}
// Actualizar estilos según zona
if (target.parentElement === this.elements.targetZone) {
target.classList.add('in-target');
} else {
target.classList.remove('in-target');
}
target.style.display = 'flex'; // o block según css base
// Limpiar DOM temporal
if (this.dragState.clone) this.dragState.clone.remove();
if (this.dragState.placeholder) this.dragState.placeholder.remove();
this.dragState = { element: null, clone: null, placeholder: null };
this.checkAutoCheck();
},
checkAutoCheck: function() {
const wordsInSource = this.elements.sourceZone.children.length;
if (wordsInSource === 0) {
this.elements.btnCheck.style.display = 'inline-flex';
// Foco automático para accesibilidad si es posible sin molestar
} else {
this.elements.btnCheck.style.display = 'none';
}
},
checkAnswer: function() {
// Obtener IDs de las palabras en el orden en que están colocadas
const placedWords = Array.from(this.elements.targetZone.querySelectorAll('.word'));
const currentOrder = placedWords.map(el => el.dataset.id);
// Orden correcto esperado
const correctOrder = this.state.currentWordsObj.map(w => w.id);
const isCorrect = currentOrder.join(',') === correctOrder.join(',');
if (isCorrect) {
this.handleSuccess();
} else {
this.handleError();
}
},
handleSuccess: function() {
this.elements.btnCheck.style.display = 'none';
this.elements.btnNext.style.display = 'inline-flex';
// Actualizar stats
if (this.state.attemptsLeft === MAX_ATTEMPTS) {
this.state.stats.firstTry++;
} else {
this.state.stats.withHelp++;
}
const msg = MOTIVATIONAL_MSGS[Math.floor(Math.random() * MOTIVATIONAL_MSGS.length)];
this.showFeedback(msg, 'success');
// Efectos
AudioSys.playSuccess();
this.createConfetti();
this.elements.targetZone.classList.add('animate-pop');
setTimeout(() => this.elements.targetZone.classList.remove('animate-pop'), 500);
// Leer frase completa si el sonido está activo
const fullSentence = this.state.currentWordsObj.map(w => w.text).join(' ');
setTimeout(() => this.speak(fullSentence), 500);
},
handleError: function() {
this.state.attemptsLeft--;
this.updateStatusBar();
AudioSys.playSoftError();
if (this.state.attemptsLeft > 0) {
this.showFeedback(`Casi lo tienes. Te quedan ${this.state.attemptsLeft} intentos. ¡Prueba a cambiar alguna palabra!`, 'warning');
// Breve animación de error
this.elements.targetZone.style.transform = 'translateX(-10px)';
setTimeout(() => this.elements.targetZone.style.transform = 'translateX(10px)', 100);
setTimeout(() => this.elements.targetZone.style.transform = 'translateX(0)', 200);
} else {
this.handleFail();
}
},
handleFail: function() {
this.state.stats.failed++;
this.elements.btnCheck.style.display = 'none';
this.elements.btnNext.style.display = 'inline-flex';
this.showFeedback("¡No te preocupes! Así es como se ordena correctamente:", 'info');
// Mostrar solución forzada
this.elements.sourceZone.innerHTML = '';
this.elements.targetZone.innerHTML = '';
this.state.currentWordsObj.forEach(wordObj => {
const el = this.createWordElement(wordObj);
el.classList.add('in-target');
el.style.pointerEvents = 'none'; // Desactivar interacción
this.elements.targetZone.appendChild(el);
});
// Destacar visualmente
this.elements.targetZone.classList.add('solution-word');
const fullSentence = this.state.currentWordsObj.map(w => w.text).join(' ');
setTimeout(() => this.speak(fullSentence), 500);
},
nextSentence: function() {
this.state.currentSentenceIndex++;
if (this.state.currentSentenceIndex < this.state.gameSentences.length) {
this.loadSentence();
} else {
this.endGame();
}
},
endGame: function() {
AudioSys.playWinGame();
this.showScreen('screen-results');
document.getElementById('stat-first').textContent = this.state.stats.firstTry;
document.getElementById('stat-help').textContent = this.state.stats.withHelp;
document.getElementById('stat-failed').textContent = this.state.stats.failed;
let motivation = "";
const total = this.state.gameSentences.length;
if (this.state.isCustomMode) {
motivation = "¡Has completado tu frase personalizada muy bien!";
} else {
if (this.state.stats.firstTry === total) {
motivation = "¡Impresionante! Has ordenado todas las frases a la perfección y sin ayuda. ¡Eres genial!";
} else if (this.state.stats.firstTry + this.state.stats.withHelp > total / 2) {
motivation = "¡Muy buen trabajo! Has completado la mayoría de las frases estupendamente.";
} else {
motivation = "¡Buen esfuerzo! Sigue practicando y cada vez lo harás más rápido.";
}
}
document.getElementById('result-motivational').textContent = motivation;
this.speak(motivation);
this.createConfetti(100);
},
confirmExit: function() {
// En lugar de usar el confirm() nativo del navegador, mostramos nuestro propio modal
document.getElementById('exit-modal').classList.remove('hidden');
AudioSys.playSoftError(); // Un sonido suave para llamar la atención
},
closeExitModal: function() {
document.getElementById('exit-modal').classList.add('hidden');
},
confirmExitAction: function() {
this.closeExitModal();
this.showScreen('screen-menu');
},
createConfetti: function(amount = 30) {
const layer = document.getElementById('animation-layer');
layer.innerHTML = '';
const colors = ['#fce18a', '#ff726d', '#b48def', '#f4306d', '#42b9f5'];
for(let i=0; i<amount; i++) {
const el = document.createElement('div');
el.className = 'particle';
el.style.left = Math.random() * 100 + 'vw';
el.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
el.style.animationDuration = (Math.random() * 1 + 1) + 's';
el.style.animationDelay = (Math.random() * 0.2) + 's';
// Pequeña aleatorización de forma
if(Math.random() > 0.5) el.style.borderRadius = '50%';
layer.appendChild(el);
// Limpiar DOM después
setTimeout(() => el.remove(), 2500);
}
}
};
// Inicializar
document.addEventListener('DOMContentLoaded', () => {
// Asegurarnos de que el modo inicial es el menú
app.showScreen('screen-menu');
});
</script>
</body>
</html>
Código html Ordenamos frases. Claude
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>¡Ordenamos las frases!</title>
<style>
/* =========================================================
ORDENAMOS LAS FRASES
Aplicación educativa para trabajar el orden sintáctico
Pensada para alumnado de Primaria con TDL (trastorno del
desarrollo del lenguaje). Un único archivo, sin dependencias
externas. Organizado en: 1) Variables y base, 2) Layout de
pantallas, 3) Componentes (tarjetas, ranuras, botones),
4) Animaciones, 5) Accesibilidad y responsive.
========================================================= */
:root{
--color-bg:#f6f4ee;
--color-bg-soft:#ffffff;
--color-primary:#1d4ed8; /* azul accesible */
--color-primary-dark:#173ea8;
--color-secondary:#0f7a5c; /* verde éxito */
--color-secondary-soft:#e3f6ee;
--color-accent:#e08a1e; /* naranja acento */
--color-accent-soft:#fdefdb;
--color-retry:#b0470f; /* naranja-rojo, se acompaña siempre de icono/texto */
--color-retry-soft:#fdeee6;
--color-text:#1f2430;
--color-text-soft:#4b5265;
--color-border:#d9d5c9;
--radius-lg:22px;
--radius-md:16px;
--radius-sm:10px;
--shadow-card:0 6px 18px rgba(31,36,48,0.10);
--shadow-card-hover:0 10px 26px rgba(31,36,48,0.16);
--focus-ring:0 0 0 4px rgba(29,78,216,0.35);
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0;
font-family:var(--font-main);
background:var(--color-bg);
color:var(--color-text);
font-size:19px;
line-height:1.5;
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{margin:0 0 .3em 0; line-height:1.2;}
p{margin:0 0 .6em 0;}
button{font-family:inherit;}
/* Foco visible siempre, para navegación por teclado */
button:focus-visible,
a:focus-visible,
[tabindex]:focus-visible{
outline:none;
box-shadow:var(--focus-ring);
}
button{cursor:pointer;}
.app{
max-width:980px;
margin:0 auto;
padding:16px;
min-height:100%;
display:flex;
flex-direction:column;
}
.screen{display:none;}
.screen.active{display:flex; flex-direction:column; flex:1; gap:18px;}
/* ---------- Barra superior común ---------- */
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
flex-wrap:wrap;
}
.icon-btn{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-sm);
padding:10px 14px;
font-size:1rem;
font-weight:600;
color:var(--color-text);
display:inline-flex;
align-items:center;
gap:8px;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease;
}
.icon-btn:hover{transform:translateY(-1px); box-shadow:var(--shadow-card-hover);}
.icon-btn:active{transform:translateY(0);}
/* ---------- Menú principal ---------- */
.menu-hero{
text-align:center;
padding:18px 10px 4px;
}
.menu-hero h1{
font-size:clamp(1.8rem, 5vw, 2.6rem);
color:var(--color-primary-dark);
}
.menu-hero .subtitle{
font-size:1.15rem;
color:var(--color-text-soft);
max-width:560px;
margin:0 auto;
}
.menu-mascot{
font-size:3.4rem;
margin-bottom:.1em;
}
.menu-buttons{
display:grid;
grid-template-columns:1fr 1fr;
gap:18px;
margin-top:10px;
}
@media (max-width:640px){
.menu-buttons{grid-template-columns:1fr;}
}
.big-choice{
background:var(--color-bg-soft);
border:3px solid var(--color-border);
border-radius:var(--radius-lg);
padding:26px 20px;
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
text-align:center;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.big-choice:hover{
transform:translateY(-3px);
box-shadow:var(--shadow-card-hover);
border-color:var(--color-primary);
}
.big-choice .emoji{font-size:2.6rem;}
.big-choice h2{font-size:1.3rem; color:var(--color-primary-dark);}
.big-choice p{color:var(--color-text-soft); font-size:.98rem;}
.howto{
background:var(--color-accent-soft);
border:2px solid var(--color-accent);
border-radius:var(--radius-md);
padding:14px 18px;
font-size:.98rem;
color:#6b4106;
}
.howto strong{color:#5a3400;}
/* ---------- Pantalla de juego ---------- */
.game-topbar{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
justify-content:space-between;
}
.progress-wrap{
display:flex;
align-items:center;
gap:10px;
flex:1;
min-width:220px;
}
.progress-label{font-weight:700; white-space:nowrap; color:var(--color-primary-dark);}
.progress-track{
flex:1;
height:14px;
background:#e7e3d6;
border-radius:999px;
overflow:hidden;
border:1px solid var(--color-border);
}
.progress-fill{
height:100%;
background:var(--color-primary);
border-radius:999px;
transition:width .4s ease;
}
.attempts-indicator{
display:flex;
align-items:center;
gap:6px;
font-weight:700;
color:var(--color-text-soft);
white-space:nowrap;
}
.heart{font-size:1.15rem;}
.heart.used{opacity:.25;}
.game-actions{display:flex; gap:8px; flex-wrap:wrap;}
.instruction-box{
text-align:center;
background:var(--color-bg-soft);
border-radius:var(--radius-md);
border:2px solid var(--color-border);
padding:16px;
}
.instruction-box h2{
font-size:1.35rem;
color:var(--color-primary-dark);
}
.instruction-box .hint{
color:var(--color-text-soft);
font-size:.95rem;
}
.answer-zone-label,.tray-zone-label{
font-weight:700;
color:var(--color-text-soft);
font-size:.95rem;
margin-bottom:6px;
display:block;
}
#slotsZone{
background:var(--color-secondary-soft);
border:3px dashed var(--color-secondary);
border-radius:var(--radius-lg);
padding:16px;
min-height:96px;
display:flex;
flex-wrap:wrap;
gap:12px;
align-items:flex-start;
transition:background .15s ease;
}
#trayZone{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-lg);
padding:16px;
min-height:96px;
display:flex;
flex-wrap:wrap;
gap:12px;
align-items:flex-start;
}
.slot{
min-width:84px;
min-height:56px;
border:2px solid #bcd9cd;
border-radius:var(--radius-md);
background:#ffffffaa;
display:flex;
align-items:center;
justify-content:center;
color:#8a9a91;
font-size:1.6rem;
padding:4px;
}
.slot.slot-hover{
border-color:var(--color-primary);
background:#eaf1ff;
}
.slot.filled{padding:0; border-style:solid; background:transparent;}
.word-card{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-md);
padding:12px 18px;
font-size:1.35rem;
font-weight:700;
color:var(--color-text);
box-shadow:var(--shadow-card);
touch-action:none;
user-select:none;
-webkit-user-select:none;
display:inline-flex;
align-items:center;
min-height:56px;
}
.word-card:hover{box-shadow:var(--shadow-card-hover); transform:translateY(-1px);}
.word-card.dragging-source{opacity:.35;}
.word-card.placed{
border-color:var(--color-primary);
background:#eef4ff;
}
.word-ghost{
position:fixed;
z-index:999;
pointer-events:none;
opacity:.92;
box-shadow:var(--shadow-card-hover);
transform:scale(1.04);
}
.feedback-banner{
border-radius:var(--radius-md);
padding:14px 18px;
font-size:1.15rem;
font-weight:700;
display:flex;
align-items:center;
gap:10px;
min-height:1.4em;
}
.feedback-banner.success{
background:var(--color-secondary-soft);
color:#0a5a41;
border:2px solid var(--color-secondary);
}
.feedback-banner.retry{
background:var(--color-retry-soft);
color:#8a3608;
border:2px solid var(--color-retry);
}
.feedback-banner.info{
background:var(--color-accent-soft);
color:#6b4106;
border:2px solid var(--color-accent);
}
.feedback-banner:empty{display:none;}
.solution-box{
background:#fff;
border:3px solid var(--color-secondary);
border-radius:var(--radius-lg);
padding:16px;
text-align:center;
}
.solution-box .solution-text{
font-size:1.5rem;
font-weight:800;
color:var(--color-secondary);
margin-top:6px;
}
.bottom-actions{
display:flex;
justify-content:center;
gap:12px;
flex-wrap:wrap;
}
.primary-btn{
background:var(--color-primary);
color:#fff;
border:none;
border-radius:var(--radius-md);
padding:14px 26px;
font-size:1.1rem;
font-weight:700;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease, background .15s ease;
}
.primary-btn:hover{background:var(--color-primary-dark); transform:translateY(-1px);}
.primary-btn:disabled{
background:#b9c3da;
cursor:not-allowed;
box-shadow:none;
transform:none;
}
.secondary-btn{
background:#fff;
color:var(--color-primary-dark);
border:2px solid var(--color-primary);
border-radius:var(--radius-md);
padding:12px 22px;
font-size:1.05rem;
font-weight:700;
}
.secondary-btn:hover{background:#eef4ff;}
/* ---------- Modo crear frase ---------- */
.custom-box{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-lg);
padding:20px;
display:flex;
flex-direction:column;
gap:12px;
}
.custom-box label{font-weight:700; color:var(--color-primary-dark); font-size:1.1rem;}
#customInput{
font-size:1.3rem;
padding:16px;
border-radius:var(--radius-md);
border:2px solid var(--color-border);
font-family:inherit;
resize:vertical;
min-height:70px;
}
#customInput:focus-visible{outline:none; box-shadow:var(--focus-ring); border-color:var(--color-primary);}
.custom-error{
color:#8a3608;
background:var(--color-retry-soft);
border:2px solid var(--color-retry);
border-radius:var(--radius-sm);
padding:8px 12px;
font-weight:600;
display:none;
}
.custom-error.show{display:block;}
/* ---------- Resultados ---------- */
.results-card{
background:var(--color-bg-soft);
border:3px solid var(--color-primary);
border-radius:var(--radius-lg);
padding:26px 22px;
text-align:center;
display:flex;
flex-direction:column;
gap:14px;
}
.results-card h2{color:var(--color-primary-dark); font-size:1.7rem;}
.stats-grid{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:12px;
margin-top:6px;
}
@media (max-width:560px){ .stats-grid{grid-template-columns:1fr;} }
.stat-box{
background:var(--color-secondary-soft);
border-radius:var(--radius-md);
padding:14px;
}
.stat-box .num{font-size:2rem; font-weight:800; color:var(--color-secondary);}
.stat-box .lbl{font-size:.9rem; color:var(--color-text-soft); font-weight:600;}
.motivation-msg{
font-size:1.2rem;
font-weight:700;
color:var(--color-primary-dark);
}
/* ---------- Animación de éxito (estrellas / confeti discreto) ---------- */
#celebrationLayer{
position:fixed;
inset:0;
pointer-events:none;
overflow:hidden;
z-index:50;
}
.confetto{
position:absolute;
top:-20px;
font-size:1.4rem;
animation:fall 1.4s ease-in forwards;
}
@keyframes fall{
to{ transform:translateY(105vh) rotate(220deg); opacity:0; }
}
.pop-badge{
position:fixed;
left:50%; top:18%;
transform:translate(-50%,-50%) scale(.4);
background:#fff;
border:3px solid var(--color-secondary);
border-radius:999px;
padding:14px 22px;
font-size:1.4rem;
font-weight:800;
color:var(--color-secondary);
box-shadow:var(--shadow-card-hover);
z-index:60;
animation:popIn .5s ease forwards;
}
@keyframes popIn{
0%{opacity:0; transform:translate(-50%,-50%) scale(.4);}
60%{opacity:1; transform:translate(-50%,-50%) scale(1.08);}
100%{opacity:1; transform:translate(-50%,-50%) scale(1);}
}
/* Respeta la preferencia de reducir movimiento */
@media (prefers-reduced-motion: reduce){
*{animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important;}
}
.sr-only{
position:absolute; width:1px; height:1px;
padding:0; margin:-1px; overflow:hidden;
clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
footer.appfoot{
text-align:center;
color:var(--color-text-soft);
font-size:.85rem;
padding:10px 0 4px;
}
</style>
</head>
<body>
<div class="app">
<!-- Región para anuncios accesibles (lectores de pantalla) -->
<div id="announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<div id="celebrationLayer" aria-hidden="true"></div>
<!-- =================== PANTALLA: MENÚ PRINCIPAL =================== -->
<section id="screen-menu" class="screen active" aria-label="Menú principal">
<div class="topbar">
<span></span>
<button id="soundToggleMenu" class="icon-btn" type="button" aria-pressed="true">🔊 Sonido</button>
</div>
<div class="menu-hero">
<div class="menu-mascot" aria-hidden="true">🦉</div>
<h1>¡Ordenamos las frases!</h1>
<p class="subtitle">Construye cada frase colocando las palabras en el orden correcto.</p>
</div>
<div class="menu-buttons">
<button class="big-choice" id="btnStartBank" type="button">
<span class="emoji" aria-hidden="true">🧩</span>
<h2>Ordenar frases</h2>
<p>Juega con 5 frases del cuaderno de frases.</p>
</button>
<button class="big-choice" id="btnStartCustom" type="button">
<span class="emoji" aria-hidden="true">✏️</span>
<h2>Crear mi propia frase</h2>
<p>Escribe una frase y ordénala.</p>
</button>
</div>
<div class="howto">
<strong>¿Cómo se juega?</strong> Toca una palabra para colocarla. Toca una palabra ya colocada para quitarla.
También puedes arrastrar las palabras con el dedo o el ratón.
</div>
<footer class="appfoot">Recurso educativo para trabajar el orden de las frases · Uso libre en el aula</footer>
</section>
<!-- =================== PANTALLA: JUEGO (frases del banco / personalizada) =================== -->
<section id="screen-game" class="screen" aria-label="Actividad de ordenar la frase">
<div class="game-topbar">
<div class="progress-wrap">
<span class="progress-label" id="progressLabel">Frase 1 de 5</span>
<div class="progress-track"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
</div>
<div class="attempts-indicator" id="attemptsIndicator" aria-hidden="true">
<span class="heart">❤️</span><span class="heart">❤️</span><span class="heart">❤️</span>
</div>
<div class="game-actions">
<button id="soundToggleGame" class="icon-btn" type="button" aria-pressed="true">🔊</button>
<button id="btnSpeakInstruction" class="icon-btn" type="button" title="Escuchar la instrucción">🗣️</button>
<button id="btnBackToMenu" class="icon-btn" type="button">🏠 Menú</button>
</div>
</div>
<div class="instruction-box">
<h2 id="instructionText">Ordena las palabras para formar la frase.</h2>
<p class="hint">Toca una palabra para colocarla en el siguiente hueco.</p>
</div>
<div>
<span class="answer-zone-label" id="slotsZoneLabel">Coloca aquí las palabras</span>
<div id="slotsZone" role="group" aria-labelledby="slotsZoneLabel"></div>
</div>
<div>
<span class="tray-zone-label" id="trayZoneLabel">Palabras disponibles</span>
<div id="trayZone" role="group" aria-labelledby="trayZoneLabel"></div>
</div>
<div id="feedbackBanner" class="feedback-banner" role="status"></div>
<div id="solutionBox" class="solution-box" style="display:none;">
<p>La frase correcta era:</p>
<p class="solution-text" id="solutionText"></p>
</div>
<div class="bottom-actions">
<button id="btnRemoveLastWord" class="secondary-btn" type="button" disabled>⌫ Borrar última palabra</button>
<button id="btnClearSlots" class="secondary-btn" type="button">↺ Borrar y empezar de nuevo</button>
<button id="btnCheck" class="primary-btn" type="button" disabled>Comprobar</button>
<button id="btnContinue" class="primary-btn" type="button" style="display:none;">Continuar →</button>
</div>
</section>
<!-- =================== PANTALLA: CREAR MI PROPIA FRASE =================== -->
<section id="screen-custom" class="screen" aria-label="Crear mi propia frase">
<div class="topbar">
<button id="btnBackToMenuFromCustom" class="icon-btn" type="button">🏠 Menú</button>
<button id="soundToggleCustom" class="icon-btn" type="button" aria-pressed="true">🔊</button>
</div>
<div class="custom-box">
<h2>Crear mis propias frases</h2>
<label for="customInput">Escribe entre 1 y 5 frases, una en cada línea:</label>
<textarea id="customInput" placeholder="El perro corre por el parque. Mamá compró pan en la tienda." aria-describedby="customError" rows="6"></textarea>
<p id="customError" class="custom-error">Escribe entre 1 y 5 frases, cada una con al menos dos palabras (una frase por línea).</p>
<button id="btnPrepareCustom" class="primary-btn" type="button">Preparar frase</button>
</div>
</section>
<!-- =================== PANTALLA: RESULTADOS =================== -->
<section id="screen-results" class="screen" aria-label="Resultados de la partida">
<div class="results-card">
<h2>¡Partida terminada!</h2>
<p class="motivation-msg" id="finalMessage"></p>
<div class="stats-grid">
<div class="stat-box"><div class="num" id="statFirstTry">0</div><div class="lbl">Acertadas a la primera</div></div>
<div class="stat-box"><div class="num" id="statWithHelp">0</div><div class="lbl">Acertadas con más intentos</div></div>
<div class="stat-box"><div class="num" id="statSolution">0</div><div class="lbl">Con solución mostrada</div></div>
</div>
<div class="bottom-actions">
<button id="btnPlayAgain" class="primary-btn" type="button">🔁 Jugar de nuevo</button>
<button id="btnBackToMenuFromResults" class="secondary-btn" type="button">🏠 Volver al menú principal</button>
</div>
</div>
</section>
</div>
<script>
(function(){
"use strict";
/* =========================================================
1) DATOS: banco de 30 frases (tal cual, con tildes y
puntuación conservadas). La puntuación queda pegada a
la última palabra para no convertirla en un elemento
independiente de la actividad.
========================================================= */
var SENTENCE_BANK = [
"Mamá hizo zumo de naranja.",
"Quiero mucho a mi tía.",
"Papá se fue a trabajar.",
"Elena es muy buena chica.",
"La leche es muy sana.",
"Me gusta el agua fresca.",
"Clara es amiga de Ana.",
"Mi abuelo vive en Madrid.",
"Pedro cuida de su hermano.",
"El soldadito perdió su gorro.",
"Tengo ojos en la cara.",
"El abuelo Paco fuma tabaco.",
"El pastel era de crema.",
"La bruja rompió su escoba.",
"Comí un flan de huevo.",
"La tarta es de chocolate.",
"La primavera la sangre altera.",
"Jaime me regaló un caballo.",
"El dragón no arrojó fuego.",
"Alberto se comió un melocotón.",
"El elefante juega al tenis.",
"Los delfines están muy contentos.",
"Diego compró una moto roja.",
"El sastre hizo un traje.",
"El agua es muy sana.",
"María participó en la carrera.",
"El gato marrón era peludo.",
"El tren llegó tarde.",
"Blancanieves se comió la manzana.",
"Miguel tiene una bicicleta nueva."
];
var SUCCESS_MESSAGES = [
"¡Muy bien!",
"¡Excelente trabajo!",
"¡Lo has conseguido!",
"¡Genial!",
"¡Qué bien has ordenado la frase!",
"¡Sigue así!",
"¡Cada vez lo haces mejor!"
];
var RETRY_MESSAGES = [
"Casi lo tienes. Prueba otra vez.",
"Vamos a intentarlo de nuevo.",
"Muy cerca. ¡Tú puedes!",
"Revisa el orden y prueba otra vez."
];
var MAX_ATTEMPTS = 3;
var SENTENCES_PER_GAME = 5;
var MAX_CUSTOM_SENTENCES = 5;
/* =========================================================
2) ESTADO DE LA APLICACIÓN
========================================================= */
var state = {
mode: "bank", // "bank" | "custom"
queue: [], // frases (texto) de esta partida
queueIndex: 0,
correctTokens: [], // palabras de la frase actual, en orden correcto
slots: [], // slots[posicion] = índice de palabra colocada, o null
trayOrder: [], // índices de palabras aún en la bandeja
placementHistory: [], // índices de palabras colocadas, en orden cronológico (la última, la más reciente)
attemptsLeft: MAX_ATTEMPTS,
results: [], // { firstTry:bool, withHelp:bool, neededSolution:bool }
soundOn: true,
reducedMotion: window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
};
var dragState = null;
/* =========================================================
3) UTILIDADES
========================================================= */
function $(id){ return document.getElementById(id); }
function shuffleArray(arr){
var a = arr.slice();
for (var i = a.length - 1; i > 0; i--){
var j = Math.floor(Math.random() * (i + 1));
var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
}
return a;
}
function pickRandom(list){
return list[Math.floor(Math.random() * list.length)];
}
function announce(text){
var el = $("announcer");
el.textContent = "";
window.setTimeout(function(){ el.textContent = text; }, 30);
}
function tokenize(sentence){
return sentence.trim().split(/\s+/).filter(function(w){ return w.length > 0; });
}
/* =========================================================
4) SONIDO (Web Audio API, sin archivos externos)
========================================================= */
var audioCtx = null;
function getAudioCtx(){
if (!audioCtx){
var AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
return audioCtx;
}
function playTone(freq, startTime, duration, gainPeak){
var ctx = getAudioCtx();
if (!ctx) return;
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.type = "sine";
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.0001, startTime);
gain.gain.exponentialRampToValueAtTime(gainPeak, startTime + 0.03);
gain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(startTime);
osc.stop(startTime + duration + 0.05);
}
function playSuccessSound(){
if (!state.soundOn) return;
var ctx = getAudioCtx();
if (!ctx) return;
var t = ctx.currentTime;
playTone(523.25, t, 0.16, 0.15); // do
playTone(659.25, t + 0.12, 0.16, 0.15); // mi
playTone(783.99, t + 0.24, 0.24, 0.15); // sol
}
function playRetrySound(){
if (!state.soundOn) return;
var ctx = getAudioCtx();
if (!ctx) return;
var t = ctx.currentTime;
playTone(330, t, 0.18, 0.09);
playTone(294, t + 0.14, 0.22, 0.09);
}
function speak(text){
if (!state.soundOn) return;
if (!("speechSynthesis" in window)) return;
try{
window.speechSynthesis.cancel();
var utter = new SpeechSynthesisUtterance(text);
utter.lang = "es-ES";
utter.rate = 0.95;
window.speechSynthesis.speak(utter);
}catch(e){ /* síntesis de voz no disponible: la app sigue funcionando */ }
}
function setSoundOn(on){
state.soundOn = on;
var label = on ? "🔊 Sonido" : "🔇 Sonido";
var labelShort = on ? "🔊" : "🔇";
[$("soundToggleMenu")].forEach(function(b){ if(b){ b.textContent = label; b.setAttribute("aria-pressed", on ? "true":"false"); }});
[$("soundToggleGame"), $("soundToggleCustom")].forEach(function(b){ if(b){ b.textContent = labelShort; b.setAttribute("aria-pressed", on ? "true":"false"); }});
}
/* =========================================================
5) NAVEGACIÓN ENTRE PANTALLAS
========================================================= */
function showScreen(id){
document.querySelectorAll(".screen").forEach(function(s){ s.classList.remove("active"); });
$(id).classList.add("active");
$(id).scrollIntoView({block:"start"});
}
/* =========================================================
6) INICIO DE PARTIDA
========================================================= */
function startBankGame(){
state.mode = "bank";
var shuffledBank = shuffleArray(SENTENCE_BANK);
state.queue = shuffledBank.slice(0, SENTENCES_PER_GAME);
state.queueIndex = 0;
state.results = [];
showScreen("screen-game");
loadSentence(state.queue[0]);
}
function startCustomGame(sentenceList){
state.mode = "custom";
state.queue = sentenceList;
state.queueIndex = 0;
state.results = [];
showScreen("screen-game");
loadSentence(state.queue[0]);
}
/* =========================================================
7) CARGA DE UNA FRASE Y RENDERIZADO
========================================================= */
function loadSentence(sentenceText){
state.correctTokens = tokenize(sentenceText);
var n = state.correctTokens.length;
state.slots = new Array(n).fill(null);
state.trayOrder = shuffleArray(state.correctTokens.map(function(_, i){ return i; }));
// Evitar que, por azar, la bandeja quede ya en el orden correcto
if (n > 1 && state.trayOrder.every(function(v,i){ return v === i; })){
var t = state.trayOrder[0]; state.trayOrder[0] = state.trayOrder[1]; state.trayOrder[1] = t;
}
state.placementHistory = [];
state.attemptsLeft = MAX_ATTEMPTS;
$("feedbackBanner").className = "feedback-banner";
$("feedbackBanner").textContent = "";
$("solutionBox").style.display = "none";
$("btnContinue").style.display = "none";
$("btnCheck").style.display = "";
$("btnCheck").disabled = true;
$("btnClearSlots").style.display = "";
$("btnRemoveLastWord").style.display = "";
updateProgressUI();
updateAttemptsUI();
renderSlots();
renderTray();
var total = state.mode === "bank" ? SENTENCES_PER_GAME : 1;
announce("Nueva frase. " + n + " palabras para ordenar.");
}
function updateProgressUI(){
var current = state.queueIndex + 1;
var total = state.queue.length;
if (state.mode === "bank"){
$("progressLabel").textContent = "Frase " + current + " de " + total;
} else {
$("progressLabel").textContent = total > 1 ? ("Mi frase " + current + " de " + total) : "Mi propia frase";
}
$("progressFill").style.width = Math.round((state.queueIndex / total) * 100) + "%";
}
function updateAttemptsUI(){
var wrap = $("attemptsIndicator");
wrap.innerHTML = "";
for (var i = 0; i < MAX_ATTEMPTS; i++){
var span = document.createElement("span");
span.className = "heart" + (i < state.attemptsLeft ? "" : " used");
span.textContent = "❤️";
wrap.appendChild(span);
}
wrap.setAttribute("aria-label", "Te quedan " + state.attemptsLeft + " de " + MAX_ATTEMPTS + " oportunidades.");
}
function renderSlots(){
var zone = $("slotsZone");
zone.innerHTML = "";
state.slots.forEach(function(wordIdx, pos){
var slot = document.createElement("div");
slot.className = "slot" + (wordIdx !== null ? " filled" : "");
slot.dataset.pos = pos;
if (wordIdx !== null){
slot.appendChild(makeWordCard(wordIdx, true));
} else {
slot.textContent = "";
slot.setAttribute("aria-label", "Hueco " + (pos + 1) + " vacío");
}
zone.appendChild(slot);
});
var allFilled = state.slots.every(function(v){ return v !== null; });
$("btnCheck").disabled = !allFilled;
$("btnRemoveLastWord").disabled = state.placementHistory.length === 0;
}
function renderTray(){
var zone = $("trayZone");
zone.innerHTML = "";
state.trayOrder.forEach(function(wordIdx){
zone.appendChild(makeWordCard(wordIdx, false));
});
}
function makeWordCard(wordIdx, isPlaced){
var card = document.createElement("div");
card.className = "word-card" + (isPlaced ? " placed" : "");
card.textContent = state.correctTokens[wordIdx];
card.dataset.wordIndex = wordIdx;
card.tabIndex = 0;
card.setAttribute("role", "button");
card.setAttribute("aria-label", (isPlaced ? "Palabra colocada: " : "Palabra: ") + state.correctTokens[wordIdx] + (isPlaced ? ". Pulsa para quitarla." : ". Pulsa para colocarla."));
card.addEventListener("pointerdown", function(e){ onWordPointerDown(e, card); });
card.addEventListener("keydown", function(e){
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
e.preventDefault();
activateWord(wordIdx, isPlaced);
}
});
return card;
}
/* =========================================================
8) INTERACCIÓN: pulsar/tocar para colocar (alternativa
accesible) + arrastrar con puntero (ratón, dedo o lápiz)
========================================================= */
function activateWord(wordIdx, isPlaced){
if (isPlaced){
removeFromSlot(wordIdx);
} else {
placeInNextEmptySlot(wordIdx);
}
}
function markPlaced(wordIdx){
state.placementHistory = state.placementHistory.filter(function(v){ return v !== wordIdx; });
state.placementHistory.push(wordIdx);
}
function markUnplaced(wordIdx){
state.placementHistory = state.placementHistory.filter(function(v){ return v !== wordIdx; });
}
function placeInNextEmptySlot(wordIdx){
var pos = state.slots.indexOf(null);
if (pos === -1) return;
state.slots[pos] = wordIdx;
state.trayOrder = state.trayOrder.filter(function(v){ return v !== wordIdx; });
markPlaced(wordIdx);
renderSlots();
renderTray();
announce("Palabra " + state.correctTokens[wordIdx] + " colocada en el hueco " + (pos + 1) + ".");
}
function placeInSlot(wordIdx, pos){
// Si el hueco ya tenía una palabra, esa palabra vuelve a la bandeja
var previousAtPos = state.slots[pos];
// Quitar wordIdx de donde estuviera (bandeja u otro hueco)
state.trayOrder = state.trayOrder.filter(function(v){ return v !== wordIdx; });
for (var i = 0; i < state.slots.length; i++){
if (state.slots[i] === wordIdx) state.slots[i] = null;
}
state.slots[pos] = wordIdx;
markPlaced(wordIdx);
if (previousAtPos !== null && previousAtPos !== wordIdx){
state.trayOrder.push(previousAtPos);
markUnplaced(previousAtPos);
}
renderSlots();
renderTray();
}
function removeFromSlot(wordIdx){
for (var i = 0; i < state.slots.length; i++){
if (state.slots[i] === wordIdx) state.slots[i] = null;
}
if (state.trayOrder.indexOf(wordIdx) === -1) state.trayOrder.push(wordIdx);
markUnplaced(wordIdx);
renderSlots();
renderTray();
announce("Palabra " + state.correctTokens[wordIdx] + " ha vuelto a las palabras disponibles.");
}
function returnToTray(wordIdx){
var wasPlaced = state.slots.indexOf(wordIdx) !== -1;
for (var i = 0; i < state.slots.length; i++){
if (state.slots[i] === wordIdx) state.slots[i] = null;
}
if (state.trayOrder.indexOf(wordIdx) === -1) state.trayOrder.push(wordIdx);
markUnplaced(wordIdx);
renderSlots();
renderTray();
if (wasPlaced) announce("Palabra devuelta a las palabras disponibles.");
}
function removeLastPlacedWord(){
if (state.placementHistory.length === 0) return;
var wordIdx = state.placementHistory[state.placementHistory.length - 1];
removeFromSlot(wordIdx);
announce("Se ha quitado la última palabra colocada.");
}
function clearSlotHighlights(){
document.querySelectorAll(".slot.slot-hover").forEach(function(s){ s.classList.remove("slot-hover"); });
}
function onWordPointerDown(e, wordEl){
if (e.button !== undefined && e.button !== 0 && e.pointerType === "mouse") return;
e.preventDefault();
var rect = wordEl.getBoundingClientRect();
dragState = {
wordEl: wordEl,
wordIndex: parseInt(wordEl.dataset.wordIndex, 10),
pointerId: e.pointerId,
startX: e.clientX,
startY: e.clientY,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top,
width: rect.width,
moved: false,
ghost: null
};
try{ wordEl.setPointerCapture(e.pointerId); }catch(err){}
wordEl.addEventListener("pointermove", onWordPointerMove);
wordEl.addEventListener("pointerup", onWordPointerUp);
wordEl.addEventListener("pointercancel", onWordPointerUp);
}
function onWordPointerMove(e){
if (!dragState) return;
var dx = e.clientX - dragState.startX;
var dy = e.clientY - dragState.startY;
if (!dragState.moved && Math.sqrt(dx*dx + dy*dy) > 10){
dragState.moved = true;
var ghost = dragState.wordEl.cloneNode(true);
ghost.className = "word-card word-ghost";
ghost.style.width = dragState.width + "px";
document.body.appendChild(ghost);
dragState.ghost = ghost;
dragState.wordEl.classList.add("dragging-source");
}
if (dragState.moved && dragState.ghost){
dragState.ghost.style.left = (e.clientX - dragState.offsetX) + "px";
dragState.ghost.style.top = (e.clientY - dragState.offsetY) + "px";
clearSlotHighlights();
var target = document.elementFromPoint(e.clientX, e.clientY);
var slot = target && target.closest(".slot");
if (slot) slot.classList.add("slot-hover");
}
}
function onWordPointerUp(e){
if (!dragState) return;
var wordEl = dragState.wordEl;
var wordIdx = dragState.wordIndex;
wordEl.removeEventListener("pointermove", onWordPointerMove);
wordEl.removeEventListener("pointerup", onWordPointerUp);
wordEl.removeEventListener("pointercancel", onWordPointerUp);
clearSlotHighlights();
if (dragState.moved){
if (dragState.ghost) dragState.ghost.remove();
wordEl.classList.remove("dragging-source");
var target = document.elementFromPoint(e.clientX, e.clientY);
var slot = target && target.closest(".slot");
var trayZone = target && target.closest("#trayZone");
if (slot){
var pos = parseInt(slot.dataset.pos, 10);
placeInSlot(wordIdx, pos);
announce("Palabra " + state.correctTokens[wordIdx] + " colocada en el hueco " + (pos + 1) + ".");
} else if (trayZone){
returnToTray(wordIdx);
}
} else {
var isPlaced = state.slots.indexOf(wordIdx) !== -1;
activateWord(wordIdx, isPlaced);
}
dragState = null;
}
/* =========================================================
9) COMPROBACIÓN DE LA RESPUESTA
========================================================= */
function checkAnswer(){
var isCorrect = state.slots.every(function(wordIdx, pos){ return wordIdx === pos; });
var attemptNumber = MAX_ATTEMPTS - state.attemptsLeft + 1;
if (isCorrect){
handleCorrectAnswer(attemptNumber);
} else {
handleWrongAnswer();
}
}
function handleCorrectAnswer(attemptNumber){
playSuccessSound();
var msg = pickRandom(SUCCESS_MESSAGES);
var banner = $("feedbackBanner");
banner.className = "feedback-banner success";
banner.textContent = "✅ " + msg;
announce(msg + " Frase correcta.");
celebrate();
state.results.push({ firstTry: attemptNumber === 1, withHelp: attemptNumber > 1, neededSolution: false });
$("btnCheck").style.display = "none";
$("btnClearSlots").style.display = "none";
$("btnRemoveLastWord").style.display = "none";
$("btnContinue").style.display = "";
$("btnContinue").focus();
}
function handleWrongAnswer(){
state.attemptsLeft -= 1;
updateAttemptsUI();
playRetrySound();
if (state.attemptsLeft > 0){
var msg = pickRandom(RETRY_MESSAGES);
var banner = $("feedbackBanner");
banner.className = "feedback-banner retry";
banner.textContent = "🔁 " + msg + " Te quedan " + state.attemptsLeft + (state.attemptsLeft === 1 ? " oportunidad." : " oportunidades.");
announce(msg);
} else {
// Se acaban los intentos: mostrar solución
state.results.push({ firstTry: false, withHelp: false, neededSolution: true });
var banner2 = $("feedbackBanner");
banner2.className = "feedback-banner info";
banner2.textContent = "💡 Vamos a ver juntos cómo se ordena esta frase. ¡Lo importante es seguir intentándolo!";
announce("Aquí tienes la frase ordenada correctamente.");
$("solutionText").textContent = state.correctTokens.join(" ");
$("solutionBox").style.display = "";
$("btnCheck").style.display = "none";
$("btnClearSlots").style.display = "none";
$("btnRemoveLastWord").style.display = "none";
$("btnContinue").style.display = "";
$("btnContinue").focus();
}
}
function resetCurrentAttemptWords(){
state.trayOrder = shuffleArray(state.correctTokens.map(function(_, i){ return i; }));
state.slots = new Array(state.correctTokens.length).fill(null);
state.placementHistory = [];
renderSlots();
renderTray();
announce("Palabras reiniciadas.");
}
/* =========================================================
10) CELEBRACIÓN (estrellas / confeti discretos)
========================================================= */
function celebrate(){
var badge = document.createElement("div");
badge.className = "pop-badge";
badge.setAttribute("aria-hidden", "true");
badge.textContent = "⭐ ¡Genial! ⭐";
document.body.appendChild(badge);
window.setTimeout(function(){ badge.remove(); }, 1200);
if (state.reducedMotion) return; // sin confeti si el usuario prefiere menos movimiento
var layer = $("celebrationLayer");
var emojis = ["⭐","✨","🎉","🌟"];
for (var i = 0; i < 10; i++){
(function(){
var piece = document.createElement("span");
piece.className = "confetto";
piece.textContent = emojis[Math.floor(Math.random()*emojis.length)];
piece.style.left = (Math.random() * 100) + "vw";
piece.style.animationDelay = (Math.random() * 0.3) + "s";
layer.appendChild(piece);
window.setTimeout(function(){ piece.remove(); }, 1800);
})();
}
}
/* =========================================================
11) AVANCE DE FRASE / FIN DE PARTIDA
========================================================= */
function goToNext(){
state.queueIndex += 1;
if (state.queueIndex >= state.queue.length){
showResults();
} else {
loadSentence(state.queue[state.queueIndex]);
}
}
function showResults(){
var firstTry = state.results.filter(function(r){ return r.firstTry; }).length;
var withHelp = state.results.filter(function(r){ return r.withHelp; }).length;
var solutionShown = state.results.filter(function(r){ return r.neededSolution; }).length;
$("statFirstTry").textContent = firstTry;
$("statWithHelp").textContent = withHelp;
$("statSolution").textContent = solutionShown;
var total = state.results.length;
var goodTotal = firstTry + withHelp;
var finalMsg;
if (goodTotal === total){
finalMsg = "¡Muy buen trabajo! Has ordenado las " + total + " frases.";
} else if (goodTotal >= Math.ceil(total * 0.6)){
finalMsg = "¡Muy buen trabajo! Has conseguido ordenar " + goodTotal + " de " + total + " frases.";
} else {
finalMsg = "¡Sigue practicando! Cada partida lo haces mejor.";
}
$("finalMessage").textContent = finalMsg;
announce(finalMsg);
if (state.mode === "custom"){
$("btnPlayAgain").textContent = "✏️ Crear otra frase";
$("btnPlayAgain").dataset.customFlow = "1";
} else {
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
delete $("btnPlayAgain").dataset.customFlow;
}
showScreen("screen-results");
}
/* =========================================================
13) EVENTOS DE INTERFAZ
========================================================= */
$("btnStartBank").addEventListener("click", function(){ startBankGame(); });
$("btnStartCustom").addEventListener("click", function(){
$("customInput").value = "";
$("customError").classList.remove("show");
showScreen("screen-custom");
window.setTimeout(function(){ $("customInput").focus(); }, 50);
});
$("btnPrepareCustom").addEventListener("click", function(){
var rawLines = $("customInput").value.split("\n");
var sentences = rawLines
.map(function(line){ return line.trim(); })
.filter(function(line){ return line.length > 0; });
var allValid = sentences.length > 0 &&
sentences.length <= MAX_CUSTOM_SENTENCES &&
sentences.every(function(s){ return tokenize(s).length >= 2; });
if (!allValid){
$("customError").classList.add("show");
announce("Escribe entre una y cinco frases, cada una con al menos dos palabras.");
return;
}
$("customError").classList.remove("show");
startCustomGame(sentences);
});
$("btnCheck").addEventListener("click", checkAnswer);
$("btnClearSlots").addEventListener("click", resetCurrentAttemptWords);
$("btnRemoveLastWord").addEventListener("click", removeLastPlacedWord);
$("btnContinue").addEventListener("click", goToNext);
$("btnBackToMenu").addEventListener("click", function(){ showScreen("screen-menu"); });
$("btnBackToMenuFromCustom").addEventListener("click", function(){ showScreen("screen-menu"); });
$("btnBackToMenuFromResults").addEventListener("click", function(){
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
delete $("btnPlayAgain").dataset.customFlow;
showScreen("screen-menu");
});
$("btnPlayAgain").addEventListener("click", function(){
if ($("btnPlayAgain").dataset.customFlow === "1"){
delete $("btnPlayAgain").dataset.customFlow;
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
$("customInput").value = "";
$("customError").classList.remove("show");
showScreen("screen-custom");
window.setTimeout(function(){ $("customInput").focus(); }, 50);
} else {
startBankGame();
}
});
$("btnSpeakInstruction").addEventListener("click", function(){
speak("Ordena las palabras para formar la frase.");
});
function bindSoundToggle(btn){
btn.addEventListener("click", function(){ setSoundOn(!state.soundOn); });
}
[$("soundToggleMenu"), $("soundToggleGame"), $("soundToggleCustom")].forEach(bindSoundToggle);
/* =========================================================
14) INICIALIZACIÓN
========================================================= */
setSoundOn(true);
showScreen("screen-menu");
})();
</script>
</body>
</html>
Aplicaciones html en un Site
Código html Ordenamos las palabras (Gemini)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>¡Ordenamos las palabras!</title>
<style>
:root {
/* Paleta de colores accesible y amigable */
--primary: #0284c7;
--primary-hover: #0369a1;
--secondary: #e0f2fe;
--bg-color: #f8fafc;
--text-color: #1e293b;
--text-light: #475569;
--success: #16a34a;
--warning: #ea580c;
--card-bg: #ffffff;
--border-color: #cbd5e1;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius: 12px;
--font-main: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* Accesibilidad: Focus visible */
:focus-visible {
outline: 4px solid var(--primary);
outline-offset: 2px;
border-radius: 4px;
}
/* Pantallas de la aplicación */
.screen {
display: none;
flex: 1;
flex-direction: column;
padding: 1rem;
max-width: 1000px;
margin: 0 auto;
width: 100%;
}
.screen.active {
display: flex;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
font-size: 1.1rem;
font-weight: bold;
}
.icon-btn {
background: none;
border: 2px solid transparent;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.icon-btn:hover { background-color: var(--secondary); }
.btn {
background-color: var(--primary);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1.25rem;
font-weight: bold;
border-radius: 50px;
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-decoration: none;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.btn:disabled {
background-color: var(--border-color);
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background-color: white;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-secondary:hover { background-color: var(--secondary); }
/* Botón de volver atrás, atractivo */
.btn-back {
background-color: white;
color: var(--primary);
border: 2px solid var(--border-color);
padding: 0.6rem 1.2rem;
border-radius: 50px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
box-shadow: var(--shadow);
text-decoration: none;
}
.btn-back:hover {
border-color: var(--primary);
background-color: var(--secondary);
transform: translateX(-4px);
}
.btn-back:focus-visible {
outline: 4px solid var(--primary);
}
.instruction {
text-align: center;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1.5rem;
color: var(--primary);
}
.zone {
min-height: 110px;
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.target-zone {
background-color: white;
border: 3px dashed var(--border-color);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
transition: border-color 0.3s;
}
.target-zone.drag-over {
border-color: var(--primary);
background-color: var(--secondary);
}
.source-zone {
background-color: var(--secondary);
border: 2px solid transparent;
}
.letter {
background-color: white;
border: 2px solid var(--border-color);
border-bottom-width: 4px;
width: 3.5rem;
height: 3.5rem;
font-size: 1.8rem;
font-weight: bold;
border-radius: 12px;
cursor: grab;
user-select: none;
touch-action: none;
box-shadow: var(--shadow);
transition: transform 0.1s, box-shadow 0.1s;
display: flex;
align-items: center;
justify-content: center;
text-transform: lowercase;
}
.letter:active {
cursor: grabbing;
}
.letter.dragging {
opacity: 0.5;
transform: scale(1.08);
box-shadow: var(--shadow-hover);
}
.letter:focus-visible {
border-color: var(--primary);
}
.letter.in-target {
border-color: var(--primary);
border-bottom-color: var(--primary-hover);
background-color: #f0fdf4;
}
.placeholder {
width: 3.5rem;
height: 3.5rem;
border: 2px dashed var(--primary);
border-radius: 12px;
background-color: rgba(2, 132, 199, 0.1);
pointer-events: none;
}
.menu-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
gap: 2rem;
}
.title {
font-size: 3rem;
color: var(--primary);
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.25rem;
color: var(--text-light);
max-width: 600px;
}
.menu-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
max-width: 400px;
}
.feedback-message {
text-align: center;
font-size: 1.5rem;
font-weight: bold;
min-height: 2.5rem;
margin-bottom: 1rem;
border-radius: var(--radius);
padding: 0.5rem;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(-10px);
}
.feedback-message.show {
opacity: 1;
transform: translateY(0);
}
.feedback-success { color: var(--success); background-color: #dcfce7; }
.feedback-warning { color: var(--warning); background-color: #ffedd5; }
.feedback-info { color: var(--primary); background-color: var(--secondary); }
.custom-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
background: white;
padding: 2rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.input-text {
width: 100%;
padding: 1rem;
font-size: 1.25rem;
border: 2px solid var(--border-color);
border-radius: 8px;
font-family: inherit;
}
.input-text:focus {
border-color: var(--primary);
outline: none;
}
.results-container {
background: white;
padding: 3rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
text-align: center;
max-width: 600px;
margin: auto;
width: 100%;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 1.25rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.stat-row:last-child { border: none; }
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.animate-pop { animation: pop 0.5s ease; }
#animation-layer {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 1000;
overflow: hidden;
}
.particle {
position: absolute;
width: 15px; height: 15px;
background-color: gold;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
animation: fall linear forwards;
}
@keyframes fall {
to { transform: translateY(100vh) rotate(360deg); opacity: 0; }
}
@media (max-width: 600px) {
.title { font-size: 2.2rem; }
.letter { width: 3rem; height: 3rem; font-size: 1.5rem; }
.btn { font-size: 1.1rem; width: 100%; }
.header-stats { flex-direction: column; gap: 0.2rem; text-align: center; }
header { flex-direction: column; gap: 1rem; }
.controls-row { display: flex; width: 100%; justify-content: space-between; align-items: center;}
}
.hidden { display: none !important; }
.solution-word { background-color: #dcfce7 !important; border-color: var(--success) !important; color: #14532d !important; }
.modal-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
backdrop-filter: blur(3px);
}
.modal-content {
background-color: white;
padding: 2.5rem;
border-radius: var(--radius);
box-shadow: var(--shadow-hover);
max-width: 90%;
width: 450px;
text-align: center;
animation: pop 0.3s ease;
}
.modal-content h2 {
color: var(--primary);
margin-bottom: 1rem;
font-size: 1.8rem;
}
.modal-content p {
color: var(--text-light);
font-size: 1.2rem;
margin-bottom: 2rem;
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 1rem;
}
@media (max-width: 600px) {
.modal-buttons { flex-direction: column; }
}
</style>
</head>
<body>
<div id="animation-layer" aria-hidden="true"></div>
<div id="aria-live-region" class="sr-only" aria-live="polite" style="position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;"></div>
<!-- Modal de confirmación para salir -->
<div id="exit-modal" class="modal-overlay hidden" aria-modal="true" role="dialog">
<div class="modal-content">
<h2>¿Volver al menú?</h2>
<p>Se perderá el progreso de esta partida. ¿Estás seguro?</p>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="app.closeExitModal()">Cancelar</button>
<button class="btn" onclick="app.confirmExitAction()">Sí, salir al menú</button>
</div>
</div>
</div>
<main id="app">
<!-- PANTALLA 1: MENÚ PRINCIPAL -->
<section id="screen-menu" class="screen active">
<div class="menu-container">
<div>
<h1 class="title">¡Ordenamos las palabras!</h1>
<p class="subtitle">Ordena las letras para formar la palabra correcta. Toca o arrastra las letras.</p>
</div>
<div class="menu-buttons">
<button class="btn" onclick="app.startGame(false, null, 1)" aria-label="Jugar nivel 1">
⭐ Nivel 1 (Palabras cortas)
</button>
<button class="btn" onclick="app.startGame(false, null, 2)" aria-label="Jugar nivel 2">
🌟 Nivel 2 (Palabras largas)
</button>
<button class="btn btn-secondary" onclick="app.showScreen('screen-custom')" aria-label="Introducir palabras propias">
✏️ Introducir palabras
</button>
</div>
<button id="btn-sound-menu" class="icon-btn" onclick="app.toggleSound()" aria-label="Activar o desactivar sonido" title="Sonido">
🔊
</button>
</div>
</section>
<!-- PANTALLA 2: INTRODUCIR PALABRAS -->
<section id="screen-custom" class="screen">
<header>
<button class="btn-back" onclick="app.showScreen('screen-menu')" aria-label="Volver al menú principal" title="Volver">
🏠 Menú
</button>
<h2>Modo personalizado</h2>
<div style="width: 110px;"></div>
</header>
<div class="custom-form">
<div style="text-align: left;">
<label style="font-size: 1.25rem; font-weight: bold; color: var(--primary);">Escribe entre 1 y 5 palabras:</label>
<p style="color: var(--text-light); margin-top: 0.25rem;">Rellena solo las casillas que necesites (mínimo 3 letras por palabra).</p>
</div>
<input type="text" id="custom-input-1" class="input-text" placeholder="Palabra 1: consola" autocomplete="off">
<input type="text" id="custom-input-2" class="input-text" placeholder="Palabra 2 (opcional)" autocomplete="off">
<input type="text" id="custom-input-3" class="input-text" placeholder="Palabra 3 (opcional)" autocomplete="off">
<input type="text" id="custom-input-4" class="input-text" placeholder="Palabra 4 (opcional)" autocomplete="off">
<input type="text" id="custom-input-5" class="input-text" placeholder="Palabra 5 (opcional)" autocomplete="off">
<div id="custom-error" style="color: var(--warning); display: none; background-color: #ffedd5; padding: 1rem; border-radius: 8px; font-weight: bold; text-align: center;">
Por favor, introduce al menos una palabra válida (de al menos 3 letras).
</div>
<button class="btn" onclick="app.prepareCustomWords()" style="margin-top: 1rem;">▶️ ¡A jugar!</button>
</div>
</section>
<!-- PANTALLA 3: JUEGO -->
<section id="screen-game" class="screen">
<header>
<div class="controls-row">
<button class="btn-back" onclick="app.confirmExit()" aria-label="Volver al menú principal" title="Volver">
🏠 Menú
</button>
<button id="btn-sound-game" class="icon-btn" onclick="app.toggleSound()" aria-label="Activar o desactivar sonido" title="Sonido">🔊</button>
</div>
<div class="header-stats">
<span id="progress-text" aria-live="polite">Palabra 1 de 5</span>
<span id="attempts-text" aria-live="polite">Oportunidades: 3</span>
</div>
</header>
<h2 class="instruction" id="game-instruction">Ordena las letras para formar la palabra.</h2>
<div id="feedback-msg" class="feedback-message" role="status"></div>
<!-- Zona donde se construye la palabra -->
<div id="target-zone" class="zone target-zone" aria-label="Coloca aquí las letras" role="list">
<!-- Las letras se insertarán aquí -->
</div>
<!-- Zona de letras disponibles -->
<div id="source-zone" class="zone source-zone" aria-label="Letras disponibles" role="list">
<!-- Las letras se insertarán aquí -->
</div>
<div style="text-align: center; margin-top: 1rem;">
<button id="btn-check" class="btn" onclick="app.checkAnswer()" style="display: none; margin: 0 auto;">Comprobar</button>
<button id="btn-next" class="btn" onclick="app.nextWord()" style="display: none; margin: 0 auto;">Siguiente palabra ➡️</button>
</div>
</section>
<!-- PANTALLA 4: RESULTADOS -->
<section id="screen-results" class="screen">
<div class="results-container">
<h2 class="title" style="font-size: 2.5rem;">¡Partida terminada!</h2>
<p id="result-motivational" class="subtitle" style="margin: 1.5rem auto; font-weight: bold; color: var(--primary);"></p>
<div style="margin-bottom: 2rem; text-align: left;">
<div class="stat-row">
<span>✅ Acertadas a la primera:</span>
<span id="stat-first" style="font-weight: bold;">0</span>
</div>
<div class="stat-row">
<span>🔄 Acertadas con varios intentos:</span>
<span id="stat-help" style="font-weight: bold;">0</span>
</div>
<div class="stat-row">
<span>💡 Necesitaron ver la solución:</span>
<span id="stat-failed" style="font-weight: bold;">0</span>
</div>
</div>
<div class="menu-buttons" style="margin: 0 auto;">
<button class="btn" onclick="app.restartGame()">Jugar de nuevo</button>
<button class="btn btn-secondary" onclick="app.showScreen('screen-menu')">Volver al menú</button>
</div>
</div>
</section>
</main>
<script>
/**
* Banco de palabras propuesto Nivel 1 (Palabras cortas)
*/
const WORD_BANK_LVL1 = [
"agua", "taxi", "brazo", "doctor", "tallo", "puerro", "guisar", "cara", "virus",
"taza", "suero", "duque", "blusa", "volar", "motor", "legua", "sonar", "cuero", "trono", "guerra", "zona",
"toro", "ganso", "ciencia", "gozar", "taller", "carbón", "buque", "lengua", "zarpar", "yeso", "huerta",
"teatro", "cordón", "trébol", "cuerpo", "juerga", "querer", "terror", "capa", "seguir"
];
/**
* Banco de palabras propuesto Nivel 2 (Palabras largas)
*/
const WORD_BANK_LVL2 = [
"consola", "aprobar", "estrecho", "linterna", "intento", "atraco", "párrafo", "forofo", "cálculo", "digestión",
"estorbar", "inmenso", "forestal", "apuro", "conservar", "fórmula", "manzano", "estrenar", "interior", "apunte",
"estrellas", "inmortal", "atorar", "aprender", "estoque", "aplastar", "esternón", "apostar", "fragata", "inmóvil",
"apestar", "formato", "apodo", "pasear", "fractura", "pasaje", "pasado", "apoyo", "interés", "apretar",
"consolar", "estrechar", "párpado"
];
const MAX_ATTEMPTS = 3;
const WORDS_PER_GAME = 5;
const MOTIVATIONAL_MSGS = [
"¡Muy bien!", "¡Excelente trabajo!", "¡Lo has conseguido!",
"¡Genial!", "¡Qué bien has ordenado las letras!", "¡Sigue así!"
];
/* Sistema de Sonido (Web Audio API) */
const AudioSys = {
ctx: null,
init: function() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if(AudioContext) this.ctx = new AudioContext();
}
},
playTone: function(freq, type, duration, vol) {
if (!app.state.soundEnabled || !this.ctx) return;
if (this.ctx.state === 'suspended') this.ctx.resume();
try {
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);
} catch(e) { console.warn("Audio error", e); }
},
playSuccess: function() {
this.init();
this.playTone(523.25, 'sine', 0.15, 0.3);
setTimeout(() => this.playTone(659.25, 'sine', 0.3, 0.3), 150);
},
playSoftError: function() {
this.init();
this.playTone(300, 'triangle', 0.2, 0.2);
},
playWinGame: function() {
this.init();
[523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 'sine', 0.2, 0.3), i * 150);
});
}
};
const app = {
state: {
isCustomMode: false,
soundEnabled: true,
currentLevel: 1,
gameWords: [],
currentWordIndex: 0,
currentLettersObj: [],
attemptsLeft: 3,
stats: { firstTry: 0, withHelp: 0, failed: 0 }
},
elements: {
targetZone: document.getElementById('target-zone'),
sourceZone: document.getElementById('source-zone'),
btnCheck: document.getElementById('btn-check'),
btnNext: document.getElementById('btn-next'),
feedbackMsg: document.getElementById('feedback-msg'),
ariaLive: document.getElementById('aria-live-region')
},
shuffleArray: function(array) {
let curId = array.length;
while (0 !== curId) {
let randId = Math.floor(Math.random() * curId);
curId -= 1;
let tmp = array[curId];
array[curId] = array[randId];
array[randId] = tmp;
}
return array;
},
speak: function(text) {
if(!this.state.soundEnabled || !window.speechSynthesis) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'es-ES';
utterance.rate = 0.9;
window.speechSynthesis.speak(utterance);
},
announce: function(message) {
this.elements.ariaLive.textContent = message;
},
toggleSound: function() {
this.state.soundEnabled = !this.state.soundEnabled;
const icon = this.state.soundEnabled ? '🔊' : '🔇';
document.getElementById('btn-sound-menu').textContent = icon;
document.getElementById('btn-sound-game').textContent = icon;
if(this.state.soundEnabled) AudioSys.init();
},
showScreen: function(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
window.scrollTo(0,0);
},
showFeedback: function(msg, type) {
const fb = this.elements.feedbackMsg;
fb.textContent = msg;
fb.className = `feedback-message show feedback-${type}`;
this.announce(msg);
},
hideFeedback: function() {
this.elements.feedbackMsg.classList.remove('show');
},
restartGame: function() {
// Reinicia la misma modalidad que se estaba jugando
if (this.state.isCustomMode) {
this.startGame(true, this.state.gameWords);
} else {
this.startGame(false, null, this.state.currentLevel);
}
},
startGame: function(isCustom, customWordsArray = null, level = 1) {
this.state.isCustomMode = isCustom;
this.state.stats = { firstTry: 0, withHelp: 0, failed: 0 };
if (isCustom && customWordsArray && customWordsArray.length > 0) {
this.state.gameWords = [...customWordsArray];
} else if (!isCustom) {
this.state.currentLevel = level;
let bankToUse = level === 1 ? WORD_BANK_LVL1 : WORD_BANK_LVL2;
let shuffledBank = this.shuffleArray([...bankToUse]);
this.state.gameWords = shuffledBank.slice(0, WORDS_PER_GAME);
}
this.state.currentWordIndex = 0;
this.showScreen('screen-game');
if(this.state.soundEnabled) AudioSys.init();
this.loadWord();
},
prepareCustomWords: function() {
const error = document.getElementById('custom-error');
let validWords = [];
for (let i = 1; i <= 5; i++) {
const input = document.getElementById(`custom-input-${i}`);
if (input) {
let text = input.value.trim().toLowerCase();
// Limpiar espacios internos si los hubiera y validar longitud mínima
text = text.replace(/\s+/g, '');
if (text.length >= 3) {
validWords.push(text);
}
}
}
if (validWords.length === 0) {
error.style.display = 'block';
document.getElementById('custom-input-1').focus();
return;
}
error.style.display = 'none';
for (let i = 1; i <= 5; i++) {
const input = document.getElementById(`custom-input-${i}`);
if (input) input.value = '';
}
this.startGame(true, validWords);
},
loadWord: function() {
this.state.attemptsLeft = MAX_ATTEMPTS;
this.hideFeedback();
this.elements.btnNext.style.display = 'none';
this.elements.btnCheck.style.display = 'none';
this.elements.targetZone.innerHTML = '';
this.elements.sourceZone.innerHTML = '';
this.elements.targetZone.classList.remove('solution-word');
this.updateStatusBar();
const word = this.state.gameWords[this.state.currentWordIndex];
const letters = word.split('');
this.state.currentLettersObj = letters.map((l, i) => ({ id: `l-${i}`, text: l }));
let scrambled = [...this.state.currentLettersObj];
let isSame = true;
let tries = 0;
while (isSame && tries < 10 && scrambled.length > 1) {
scrambled = this.shuffleArray(scrambled);
isSame = scrambled.every((l, i) => l.id === this.state.currentLettersObj[i].id);
tries++;
}
scrambled.forEach(letterObj => {
const el = this.createLetterElement(letterObj);
this.elements.sourceZone.appendChild(el);
});
this.announce(`Nueva palabra. Tienes ${this.state.currentLettersObj.length} letras para ordenar.`);
this.checkAutoCheck();
},
updateStatusBar: function() {
const total = this.state.gameWords.length;
const current = this.state.currentWordIndex + 1;
document.getElementById('progress-text').textContent = `Palabra ${current} de ${total}`;
document.getElementById('attempts-text').textContent = `Oportunidades: ${this.state.attemptsLeft}`;
},
createLetterElement: function(letterObj) {
const div = document.createElement('div');
div.className = 'letter';
div.textContent = letterObj.text;
div.dataset.id = letterObj.id;
div.tabIndex = 0;
div.setAttribute('role', 'button');
div.setAttribute('aria-pressed', 'false');
div.addEventListener('click', () => {
this.handleLetterClick(div);
});
div.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleLetterClick(div);
}
});
div.addEventListener('pointerdown', this.handlePointerDown.bind(this));
return div;
},
handleLetterClick: function(letterEl) {
if (letterEl.parentElement === this.elements.sourceZone) {
this.elements.targetZone.appendChild(letterEl);
letterEl.classList.add('in-target');
AudioSys.init();
} else {
this.elements.sourceZone.appendChild(letterEl);
letterEl.classList.remove('in-target');
}
this.checkAutoCheck();
},
dragState: { element: null, clone: null, placeholder: null, offsetX: 0, offsetY: 0 },
handlePointerDown: function(e) {
if (e.button !== 0 && e.pointerType === 'mouse') return;
const target = e.target.closest('.letter');
if (!target) return;
e.preventDefault();
target.setPointerCapture(e.pointerId);
this.dragState.element = target;
const rect = target.getBoundingClientRect();
this.dragState.offsetX = e.clientX - rect.left;
this.dragState.offsetY = e.clientY - rect.top;
const clone = target.cloneNode(true);
clone.classList.add('dragging');
clone.style.position = 'fixed';
clone.style.left = rect.left + 'px';
clone.style.top = rect.top + 'px';
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
clone.style.zIndex = '1000';
clone.style.pointerEvents = 'none';
document.body.appendChild(clone);
this.dragState.clone = clone;
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
placeholder.style.width = rect.width + 'px';
placeholder.style.height = rect.height + 'px';
this.dragState.placeholder = placeholder;
target.parentNode.insertBefore(placeholder, target);
target.style.display = 'none';
target.addEventListener('pointermove', this.handlePointerMove.bind(this));
target.addEventListener('pointerup', this.handlePointerUp.bind(this));
target.addEventListener('pointercancel', this.handlePointerUp.bind(this));
},
handlePointerMove: function(e) {
if (!this.dragState.clone) return;
const x = e.clientX - this.dragState.offsetX;
const y = e.clientY - this.dragState.offsetY;
this.dragState.clone.style.left = x + 'px';
this.dragState.clone.style.top = y + 'px';
this.dragState.clone.style.display = 'none';
const elemBelow = document.elementFromPoint(e.clientX, e.clientY);
this.dragState.clone.style.display = 'flex';
if (!elemBelow) return;
const dropZone = elemBelow.closest('.zone');
document.querySelectorAll('.zone').forEach(z => z.classList.remove('drag-over'));
if (dropZone) {
dropZone.classList.add('drag-over');
const siblings = [...dropZone.querySelectorAll('.letter:not([style*="display: none"]), .placeholder')];
const nextSibling = siblings.find(sibling => {
const box = sibling.getBoundingClientRect();
return e.clientX < box.left + box.width / 2;
});
if (nextSibling !== this.dragState.placeholder) {
dropZone.insertBefore(this.dragState.placeholder, nextSibling);
}
}
},
handlePointerUp: function(e) {
if (!this.dragState.element) return;
const target = this.dragState.element;
target.releasePointerCapture(e.pointerId);
target.removeEventListener('pointermove', this.handlePointerMove);
target.removeEventListener('pointerup', this.handlePointerUp);
target.removeEventListener('pointercancel', this.handlePointerUp);
document.querySelectorAll('.zone').forEach(z => z.classList.remove('drag-over'));
if (this.dragState.placeholder.parentNode) {
this.dragState.placeholder.parentNode.insertBefore(target, this.dragState.placeholder);
}
if (target.parentElement === this.elements.targetZone) {
target.classList.add('in-target');
} else {
target.classList.remove('in-target');
}
target.style.display = 'flex';
if (this.dragState.clone) this.dragState.clone.remove();
if (this.dragState.placeholder) this.dragState.placeholder.remove();
this.dragState = { element: null, clone: null, placeholder: null };
this.checkAutoCheck();
},
checkAutoCheck: function() {
const lettersInSource = this.elements.sourceZone.children.length;
if (lettersInSource === 0) {
this.elements.btnCheck.style.display = 'inline-flex';
} else {
this.elements.btnCheck.style.display = 'none';
}
},
checkAnswer: function() {
const placedLetters = Array.from(this.elements.targetZone.querySelectorAll('.letter'));
// Extraemos el texto de cada letra colocada y lo unimos para formar la palabra
const currentWord = placedLetters.map(el => el.textContent).join('').toLowerCase();
// Obtenemos la palabra correcta actual
const correctWord = this.state.gameWords[this.state.currentWordIndex].toLowerCase();
// Comparamos la palabra formada con la palabra correcta
const isCorrect = currentWord === correctWord;
if (isCorrect) {
this.handleSuccess();
} else {
this.handleError();
}
},
handleSuccess: function() {
this.elements.btnCheck.style.display = 'none';
this.elements.btnNext.style.display = 'inline-flex';
if (this.state.attemptsLeft === MAX_ATTEMPTS) {
this.state.stats.firstTry++;
} else {
this.state.stats.withHelp++;
}
const msg = MOTIVATIONAL_MSGS[Math.floor(Math.random() * MOTIVATIONAL_MSGS.length)];
this.showFeedback(msg, 'success');
AudioSys.playSuccess();
this.createConfetti();
this.elements.targetZone.classList.add('animate-pop');
setTimeout(() => this.elements.targetZone.classList.remove('animate-pop'), 500);
const fullWord = this.state.gameWords[this.state.currentWordIndex];
setTimeout(() => this.speak(fullWord), 500);
},
handleError: function() {
this.state.attemptsLeft--;
this.updateStatusBar();
AudioSys.playSoftError();
if (this.state.attemptsLeft > 0) {
this.showFeedback(`Casi lo tienes. Te quedan ${this.state.attemptsLeft} intentos. ¡Prueba a cambiar alguna letra!`, 'warning');
this.elements.targetZone.style.transform = 'translateX(-10px)';
setTimeout(() => this.elements.targetZone.style.transform = 'translateX(10px)', 100);
setTimeout(() => this.elements.targetZone.style.transform = 'translateX(0)', 200);
} else {
this.handleFail();
}
},
handleFail: function() {
this.state.stats.failed++;
this.elements.btnCheck.style.display = 'none';
this.elements.btnNext.style.display = 'inline-flex';
this.showFeedback("¡No te preocupes! Así es como se ordena correctamente:", 'info');
this.elements.sourceZone.innerHTML = '';
this.elements.targetZone.innerHTML = '';
this.state.currentLettersObj.forEach(letterObj => {
const el = this.createLetterElement(letterObj);
el.classList.add('in-target');
el.style.pointerEvents = 'none';
this.elements.targetZone.appendChild(el);
});
this.elements.targetZone.classList.add('solution-word');
const fullWord = this.state.gameWords[this.state.currentWordIndex];
setTimeout(() => this.speak(fullWord), 500);
},
nextWord: function() {
this.state.currentWordIndex++;
if (this.state.currentWordIndex < this.state.gameWords.length) {
this.loadWord();
} else {
this.endGame();
}
},
endGame: function() {
AudioSys.playWinGame();
this.showScreen('screen-results');
document.getElementById('stat-first').textContent = this.state.stats.firstTry;
document.getElementById('stat-help').textContent = this.state.stats.withHelp;
document.getElementById('stat-failed').textContent = this.state.stats.failed;
let motivation = "";
const total = this.state.gameWords.length;
if (this.state.isCustomMode) {
motivation = "¡Has completado tus palabras personalizadas estupendamente!";
} else {
if (this.state.stats.firstTry === total) {
motivation = "¡Impresionante! Has ordenado todas las palabras a la perfección y sin ayuda. ¡Eres genial!";
} else if (this.state.stats.firstTry + this.state.stats.withHelp > total / 2) {
motivation = "¡Muy buen trabajo! Has completado la mayoría de las palabras fenomenal.";
} else {
motivation = "¡Buen esfuerzo! Sigue practicando y cada vez lo harás más rápido.";
}
}
document.getElementById('result-motivational').textContent = motivation;
this.speak(motivation);
this.createConfetti(100);
},
confirmExit: function() {
document.getElementById('exit-modal').classList.remove('hidden');
AudioSys.playSoftError();
},
closeExitModal: function() {
document.getElementById('exit-modal').classList.add('hidden');
},
confirmExitAction: function() {
this.closeExitModal();
this.showScreen('screen-menu');
},
createConfetti: function(amount = 30) {
const layer = document.getElementById('animation-layer');
layer.innerHTML = '';
const colors = ['#fce18a', '#ff726d', '#b48def', '#f4306d', '#42b9f5'];
for(let i=0; i<amount; i++) {
const el = document.createElement('div');
el.className = 'particle';
el.style.left = Math.random() * 100 + 'vw';
el.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
el.style.animationDuration = (Math.random() * 1 + 1) + 's';
el.style.animationDelay = (Math.random() * 0.2) + 's';
if(Math.random() > 0.5) el.style.borderRadius = '50%';
layer.appendChild(el);
setTimeout(() => el.remove(), 2500);
}
}
};
document.addEventListener('DOMContentLoaded', () => {
app.showScreen('screen-menu');
});
</script>
</body>
</html>
Código html Ordenamos las palabras (Claude)
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>¡Ordenamos las palabras!</title>
<style>
/* =========================================================
ORDENAMOS LAS PALABRAS
Aplicación educativa para trabajar la ortografía y la
estructura de las palabras mediante el ordenamiento de
letras desordenadas. Pensada para alumnado de Primaria con
TDL (trastorno del desarrollo del lenguaje). Un único
archivo, sin dependencias externas. Organizado en:
1) Variables y base, 2) Layout de pantallas, 3) Componentes
(tarjetas de letra, ranuras, botones), 4) Animaciones,
5) Accesibilidad y responsive.
========================================================= */
:root{
--color-bg:#f6f4ee;
--color-bg-soft:#ffffff;
--color-primary:#1d4ed8; /* azul accesible */
--color-primary-dark:#173ea8;
--color-secondary:#0f7a5c; /* verde éxito */
--color-secondary-soft:#e3f6ee;
--color-accent:#e08a1e; /* naranja acento */
--color-accent-soft:#fdefdb;
--color-retry:#b0470f; /* naranja-rojo, se acompaña siempre de icono/texto */
--color-retry-soft:#fdeee6;
--color-text:#1f2430;
--color-text-soft:#4b5265;
--color-border:#d9d5c9;
--radius-lg:22px;
--radius-md:16px;
--radius-sm:10px;
--shadow-card:0 6px 18px rgba(31,36,48,0.10);
--shadow-card-hover:0 10px 26px rgba(31,36,48,0.16);
--focus-ring:0 0 0 4px rgba(29,78,216,0.35);
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
*{box-sizing:border-box;}
html,body{height:100%;}
body{
margin:0;
font-family:var(--font-main);
background:var(--color-bg);
color:var(--color-text);
font-size:19px;
line-height:1.5;
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{margin:0 0 .3em 0; line-height:1.2;}
p{margin:0 0 .6em 0;}
button{font-family:inherit;}
button:focus-visible,
a:focus-visible,
[tabindex]:focus-visible{
outline:none;
box-shadow:var(--focus-ring);
}
button{cursor:pointer;}
.app{
max-width:980px;
margin:0 auto;
padding:16px;
min-height:100%;
display:flex;
flex-direction:column;
}
.screen{display:none;}
.screen.active{display:flex; flex-direction:column; flex:1; gap:18px;}
/* ---------- Barra superior común ---------- */
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
flex-wrap:wrap;
}
.icon-btn{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-sm);
padding:10px 14px;
font-size:1rem;
font-weight:600;
color:var(--color-text);
display:inline-flex;
align-items:center;
gap:8px;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease;
}
.icon-btn:hover{transform:translateY(-1px); box-shadow:var(--shadow-card-hover);}
.icon-btn:active{transform:translateY(0);}
/* ---------- Menú principal ---------- */
.menu-hero{
text-align:center;
padding:18px 10px 4px;
}
.menu-hero h1{
font-size:clamp(1.8rem, 5vw, 2.6rem);
color:var(--color-primary-dark);
}
.menu-hero .subtitle{
font-size:1.15rem;
color:var(--color-text-soft);
max-width:560px;
margin:0 auto;
}
.menu-mascot{
font-size:3.4rem;
margin-bottom:.1em;
}
.menu-buttons{
display:grid;
grid-template-columns:1fr 1fr;
gap:18px;
margin-top:10px;
}
@media (max-width:640px){
.menu-buttons{grid-template-columns:1fr;}
}
.big-choice{
background:var(--color-bg-soft);
border:3px solid var(--color-border);
border-radius:var(--radius-lg);
padding:26px 20px;
display:flex;
flex-direction:column;
align-items:center;
gap:10px;
text-align:center;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.big-choice:hover{
transform:translateY(-3px);
box-shadow:var(--shadow-card-hover);
border-color:var(--color-primary);
}
.big-choice .emoji{font-size:2.6rem;}
.big-choice h2{font-size:1.3rem; color:var(--color-primary-dark);}
.big-choice p{color:var(--color-text-soft); font-size:.98rem;}
.howto{
background:var(--color-accent-soft);
border:2px solid var(--color-accent);
border-radius:var(--radius-md);
padding:14px 18px;
font-size:.98rem;
color:#6b4106;
}
.howto strong{color:#5a3400;}
/* ---------- Pantalla de juego ---------- */
.game-topbar{
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:center;
justify-content:space-between;
}
.progress-wrap{
display:flex;
align-items:center;
gap:10px;
flex:1;
min-width:220px;
}
.progress-label{font-weight:700; white-space:nowrap; color:var(--color-primary-dark);}
.progress-track{
flex:1;
height:14px;
background:#e7e3d6;
border-radius:999px;
overflow:hidden;
border:1px solid var(--color-border);
}
.progress-fill{
height:100%;
background:var(--color-primary);
border-radius:999px;
transition:width .4s ease;
}
.attempts-indicator{
display:flex;
align-items:center;
gap:6px;
font-weight:700;
color:var(--color-text-soft);
white-space:nowrap;
}
.heart{font-size:1.15rem;}
.heart.used{opacity:.25;}
.game-actions{display:flex; gap:8px; flex-wrap:wrap;}
.instruction-box{
text-align:center;
background:var(--color-bg-soft);
border-radius:var(--radius-md);
border:2px solid var(--color-border);
padding:16px;
}
.instruction-box h2{
font-size:1.35rem;
color:var(--color-primary-dark);
}
.instruction-box .hint{
color:var(--color-text-soft);
font-size:.95rem;
}
.answer-zone-label,.tray-zone-label{
font-weight:700;
color:var(--color-text-soft);
font-size:.95rem;
margin-bottom:6px;
display:block;
}
#slotsZone{
background:var(--color-secondary-soft);
border:3px dashed var(--color-secondary);
border-radius:var(--radius-lg);
padding:16px;
min-height:88px;
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:flex-start;
justify-content:center;
transition:background .15s ease;
}
#trayZone{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-lg);
padding:16px;
min-height:88px;
display:flex;
flex-wrap:wrap;
gap:10px;
align-items:flex-start;
justify-content:center;
}
.slot{
width:56px;
height:56px;
border:2px solid #bcd9cd;
border-radius:var(--radius-md);
background:#ffffffaa;
display:flex;
align-items:center;
justify-content:center;
color:#8a9a91;
font-size:1.6rem;
padding:2px;
}
.slot.slot-hover{
border-color:var(--color-primary);
background:#eaf1ff;
}
.slot.filled{padding:0; border-style:solid; background:transparent;}
.letter-card{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-md);
width:56px;
height:56px;
font-size:1.7rem;
font-weight:800;
color:var(--color-text);
box-shadow:var(--shadow-card);
touch-action:none;
user-select:none;
-webkit-user-select:none;
display:inline-flex;
align-items:center;
justify-content:center;
text-transform:uppercase;
}
.letter-card:hover{box-shadow:var(--shadow-card-hover); transform:translateY(-1px);}
.letter-card.dragging-source{opacity:.35;}
.letter-card.placed{
border-color:var(--color-primary);
background:#eef4ff;
}
.letter-ghost{
position:fixed;
z-index:999;
pointer-events:none;
opacity:.92;
box-shadow:var(--shadow-card-hover);
transform:scale(1.06);
}
.feedback-banner{
border-radius:var(--radius-md);
padding:14px 18px;
font-size:1.15rem;
font-weight:700;
display:flex;
align-items:center;
gap:10px;
min-height:1.4em;
}
.feedback-banner.success{
background:var(--color-secondary-soft);
color:#0a5a41;
border:2px solid var(--color-secondary);
}
.feedback-banner.retry{
background:var(--color-retry-soft);
color:#8a3608;
border:2px solid var(--color-retry);
}
.feedback-banner.info{
background:var(--color-accent-soft);
color:#6b4106;
border:2px solid var(--color-accent);
}
.feedback-banner:empty{display:none;}
.solution-box{
background:#fff;
border:3px solid var(--color-secondary);
border-radius:var(--radius-lg);
padding:16px;
text-align:center;
}
.solution-box .solution-text{
font-size:1.8rem;
font-weight:800;
letter-spacing:2px;
color:var(--color-secondary);
margin-top:6px;
text-transform:uppercase;
}
.bottom-actions{
display:flex;
justify-content:center;
gap:12px;
flex-wrap:wrap;
}
.primary-btn{
background:var(--color-primary);
color:#fff;
border:none;
border-radius:var(--radius-md);
padding:14px 26px;
font-size:1.1rem;
font-weight:700;
box-shadow:var(--shadow-card);
transition:transform .15s ease, box-shadow .15s ease, background .15s ease;
}
.primary-btn:hover{background:var(--color-primary-dark); transform:translateY(-1px);}
.primary-btn:disabled{
background:#b9c3da;
cursor:not-allowed;
box-shadow:none;
transform:none;
}
.secondary-btn{
background:#fff;
color:var(--color-primary-dark);
border:2px solid var(--color-primary);
border-radius:var(--radius-md);
padding:12px 22px;
font-size:1.05rem;
font-weight:700;
}
.secondary-btn:hover{background:#eef4ff;}
.secondary-btn:disabled{
opacity:.45;
cursor:not-allowed;
}
/* ---------- Modo introducir palabras ---------- */
.custom-box{
background:var(--color-bg-soft);
border:2px solid var(--color-border);
border-radius:var(--radius-lg);
padding:20px;
display:flex;
flex-direction:column;
gap:12px;
}
.custom-box label{font-weight:700; color:var(--color-primary-dark); font-size:1.1rem;}
#customInput{
font-size:1.3rem;
padding:16px;
border-radius:var(--radius-md);
border:2px solid var(--color-border);
font-family:inherit;
resize:vertical;
min-height:70px;
}
#customInput:focus-visible{outline:none; box-shadow:var(--focus-ring); border-color:var(--color-primary);}
.custom-error{
color:#8a3608;
background:var(--color-retry-soft);
border:2px solid var(--color-retry);
border-radius:var(--radius-sm);
padding:8px 12px;
font-weight:600;
display:none;
}
.custom-error.show{display:block;}
/* ---------- Resultados ---------- */
.results-card{
background:var(--color-bg-soft);
border:3px solid var(--color-primary);
border-radius:var(--radius-lg);
padding:26px 22px;
text-align:center;
display:flex;
flex-direction:column;
gap:14px;
}
.results-card h2{color:var(--color-primary-dark); font-size:1.7rem;}
.stats-grid{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:12px;
margin-top:6px;
}
@media (max-width:560px){ .stats-grid{grid-template-columns:1fr;} }
.stat-box{
background:var(--color-secondary-soft);
border-radius:var(--radius-md);
padding:14px;
}
.stat-box .num{font-size:2rem; font-weight:800; color:var(--color-secondary);}
.stat-box .lbl{font-size:.9rem; color:var(--color-text-soft); font-weight:600;}
.motivation-msg{
font-size:1.2rem;
font-weight:700;
color:var(--color-primary-dark);
}
/* ---------- Animación de éxito (estrellas / confeti discreto) ---------- */
#celebrationLayer{
position:fixed;
inset:0;
pointer-events:none;
overflow:hidden;
z-index:50;
}
.confetto{
position:absolute;
top:-20px;
font-size:1.4rem;
animation:fall 1.4s ease-in forwards;
}
@keyframes fall{
to{ transform:translateY(105vh) rotate(220deg); opacity:0; }
}
.pop-badge{
position:fixed;
left:50%; top:18%;
transform:translate(-50%,-50%) scale(.4);
background:#fff;
border:3px solid var(--color-secondary);
border-radius:999px;
padding:14px 22px;
font-size:1.4rem;
font-weight:800;
color:var(--color-secondary);
box-shadow:var(--shadow-card-hover);
z-index:60;
animation:popIn .5s ease forwards;
}
@keyframes popIn{
0%{opacity:0; transform:translate(-50%,-50%) scale(.4);}
60%{opacity:1; transform:translate(-50%,-50%) scale(1.08);}
100%{opacity:1; transform:translate(-50%,-50%) scale(1);}
}
@media (prefers-reduced-motion: reduce){
*{animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important;}
}
.sr-only{
position:absolute; width:1px; height:1px;
padding:0; margin:-1px; overflow:hidden;
clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
footer.appfoot{
text-align:center;
color:var(--color-text-soft);
font-size:.85rem;
padding:10px 0 4px;
}
</style>
</head>
<body>
<div class="app">
<!-- Región para anuncios accesibles (lectores de pantalla) -->
<div id="announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<div id="celebrationLayer" aria-hidden="true"></div>
<!-- =================== PANTALLA: MENÚ PRINCIPAL =================== -->
<section id="screen-menu" class="screen active" aria-label="Menú principal">
<div class="topbar">
<span></span>
<button id="soundToggleMenu" class="icon-btn" type="button" aria-pressed="true">🔊 Sonido</button>
</div>
<div class="menu-hero">
<div class="menu-mascot" aria-hidden="true">🦊</div>
<h1>¡Ordenamos las palabras!</h1>
<p class="subtitle">Coloca las letras en el orden correcto para formar cada palabra.</p>
</div>
<div class="menu-buttons">
<button class="big-choice" id="btnStartBank" type="button">
<span class="emoji" aria-hidden="true">🔤</span>
<h2>Ordenar palabras</h2>
<p>Juega con 5 palabras de la lista.</p>
</button>
<button class="big-choice" id="btnStartCustom" type="button">
<span class="emoji" aria-hidden="true">✏️</span>
<h2>Introducir palabras</h2>
<p>Escribe tus propias palabras y ordénalas.</p>
</button>
</div>
<div class="howto">
<strong>¿Cómo se juega?</strong> Toca una letra para colocarla. Toca una letra ya colocada para quitarla.
También puedes arrastrar las letras con el dedo o el ratón.
</div>
<footer class="appfoot">Recurso educativo para trabajar el orden de las letras · Uso libre en el aula</footer>
</section>
<!-- =================== PANTALLA: ELEGIR NIVEL =================== -->
<section id="screen-level" class="screen" aria-label="Elegir nivel">
<div class="topbar">
<button id="btnBackToMenuFromLevel" class="icon-btn" type="button">🏠 Menú</button>
<span></span>
</div>
<div class="menu-hero">
<h1>¿Qué nivel quieres jugar?</h1>
<p class="subtitle">Elige el nivel de dificultad de las palabras.</p>
</div>
<div class="menu-buttons">
<button class="big-choice" id="btnLevel1" type="button">
<span class="emoji" aria-hidden="true">🥉</span>
<h2>Nivel 1</h2>
<p>Palabras más cortas y sencillas.</p>
</button>
<button class="big-choice" id="btnLevel2" type="button">
<span class="emoji" aria-hidden="true">🥈</span>
<h2>Nivel 2</h2>
<p>Palabras más largas y complejas.</p>
</button>
</div>
</section>
<!-- =================== PANTALLA: JUEGO (palabras de la lista / introducidas) =================== -->
<section id="screen-game" class="screen" aria-label="Actividad de ordenar la palabra">
<div class="game-topbar">
<div class="progress-wrap">
<span class="progress-label" id="progressLabel">Palabra 1 de 5</span>
<div class="progress-track"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
</div>
<div class="attempts-indicator" id="attemptsIndicator" aria-hidden="true">
<span class="heart">❤️</span><span class="heart">❤️</span><span class="heart">❤️</span>
</div>
<div class="game-actions">
<button id="soundToggleGame" class="icon-btn" type="button" aria-pressed="true">🔊</button>
<button id="btnSpeakInstruction" class="icon-btn" type="button" title="Escuchar la instrucción">🗣️</button>
<button id="btnBackToMenu" class="icon-btn" type="button">🏠 Menú</button>
</div>
</div>
<div class="instruction-box">
<h2 id="instructionText">Ordena las letras para formar la palabra.</h2>
<p class="hint">Toca una letra para colocarla en el siguiente hueco.</p>
</div>
<div>
<span class="answer-zone-label" id="slotsZoneLabel">Coloca aquí las letras</span>
<div id="slotsZone" role="group" aria-labelledby="slotsZoneLabel"></div>
</div>
<div>
<span class="tray-zone-label" id="trayZoneLabel">Letras disponibles</span>
<div id="trayZone" role="group" aria-labelledby="trayZoneLabel"></div>
</div>
<div id="feedbackBanner" class="feedback-banner" role="status"></div>
<div id="solutionBox" class="solution-box" style="display:none;">
<p>La palabra correcta era:</p>
<p class="solution-text" id="solutionText"></p>
</div>
<div class="bottom-actions">
<button id="btnRemoveLastWord" class="secondary-btn" type="button" disabled>⌫ Borrar última letra</button>
<button id="btnClearSlots" class="secondary-btn" type="button">↺ Borrar y empezar de nuevo</button>
<button id="btnCheck" class="primary-btn" type="button" disabled>Comprobar</button>
<button id="btnContinue" class="primary-btn" type="button" style="display:none;">Continuar →</button>
</div>
</section>
<!-- =================== PANTALLA: INTRODUCIR PALABRAS =================== -->
<section id="screen-custom" class="screen" aria-label="Introducir mis palabras">
<div class="topbar">
<button id="btnBackToMenuFromCustom" class="icon-btn" type="button">🏠 Menú</button>
<button id="soundToggleCustom" class="icon-btn" type="button" aria-pressed="true">🔊</button>
</div>
<div class="custom-box">
<h2>Introducir mis palabras</h2>
<label for="customInput">Escribe entre 1 y 5 palabras, una en cada línea:</label>
<textarea id="customInput" placeholder="elefante montaña" aria-describedby="customError" rows="6"></textarea>
<p id="customError" class="custom-error">Escribe entre 1 y 5 palabras, cada una con al menos 3 letras (solo letras, sin espacios, una por línea).</p>
<button id="btnPrepareCustom" class="primary-btn" type="button">Preparar palabra</button>
</div>
</section>
<!-- =================== PANTALLA: RESULTADOS =================== -->
<section id="screen-results" class="screen" aria-label="Resultados de la partida">
<div class="results-card">
<h2>¡Partida terminada!</h2>
<p class="motivation-msg" id="finalMessage"></p>
<div class="stats-grid">
<div class="stat-box"><div class="num" id="statFirstTry">0</div><div class="lbl">Acertadas a la primera</div></div>
<div class="stat-box"><div class="num" id="statWithHelp">0</div><div class="lbl">Acertadas con más intentos</div></div>
<div class="stat-box"><div class="num" id="statSolution">0</div><div class="lbl">Con solución mostrada</div></div>
</div>
<div class="bottom-actions">
<button id="btnPlayAgain" class="primary-btn" type="button">🔁 Jugar de nuevo</button>
<button id="btnBackToMenuFromResults" class="secondary-btn" type="button">🏠 Volver al menú principal</button>
</div>
</div>
</section>
</div>
<script>
(function(){
"use strict";
/* =========================================================
1) DATOS: lista de palabras propuestas
========================================================= */
var WORD_BANK_LEVEL_1 = [
"agua","taxi","brazo","doctor","tallo","puerro","guisar","cara","virus",
"taza","suero","duque","blusa","volar","motor","legua","sonar","cuero","trono","guerra","zona",
"toro","ganso","ciencia","gozar","taller","carbón","buque","lengua","zarpar","yeso","huerta",
"teatro","cordón","trébol","cuerpo","juerga","querer","terror","capa","seguir"
];
var WORD_BANK_LEVEL_2 = [
"consola","aprobar","estrecho","linterna","intento","atraco","párrafo","forofo",
"cálculo","digestión","estorbar","inmenso","forestal","apuro","conservar","fórmula",
"manzano","estrenar","interior","apunte","estrellas","inmortal","atorar","aprender",
"estoque","aplastar","esternón","apostar","fragata","inmóvil","apestar","formato",
"apodo","pasear","fractura","pasaje","pasado","apoyo","interés","apretar",
"consolar","estrechar","párpado"
];
var SUCCESS_MESSAGES = [
"¡Muy bien!",
"¡Excelente trabajo!",
"¡Lo has conseguido!",
"¡Genial!",
"¡Qué bien has ordenado la palabra!",
"¡Sigue así!",
"¡Cada vez lo haces mejor!"
];
var RETRY_MESSAGES = [
"Casi lo tienes. Prueba otra vez.",
"Vamos a intentarlo de nuevo.",
"Muy cerca. ¡Tú puedes!",
"Revisa el orden y prueba otra vez."
];
var MAX_ATTEMPTS = 3;
var WORDS_PER_GAME = 5;
var MAX_CUSTOM_WORDS = 5;
var SPANISH_LETTERS_RE = /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ]+$/;
/* =========================================================
2) ESTADO DE LA APLICACIÓN
========================================================= */
var state = {
mode: "bank", // "bank" | "custom"
level: 1, // 1 | 2 (solo aplica al modo "bank")
queue: [], // palabras (texto) de esta partida
queueIndex: 0,
correctLetters: [], // letras de la palabra actual, en orden correcto
slots: [], // slots[posicion] = índice de letra colocada, o null
trayOrder: [], // índices de letras aún en la bandeja
placementHistory: [], // índices de letras colocadas, en orden cronológico (la última, la más reciente)
attemptsLeft: MAX_ATTEMPTS,
results: [], // { firstTry:bool, withHelp:bool, neededSolution:bool }
soundOn: true,
reducedMotion: window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
};
var dragState = null;
/* =========================================================
3) UTILIDADES
========================================================= */
function $(id){ return document.getElementById(id); }
function shuffleArray(arr){
var a = arr.slice();
for (var i = a.length - 1; i > 0; i--){
var j = Math.floor(Math.random() * (i + 1));
var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
}
return a;
}
function pickRandom(list){
return list[Math.floor(Math.random() * list.length)];
}
function announce(text){
var el = $("announcer");
el.textContent = "";
window.setTimeout(function(){ el.textContent = text; }, 30);
}
function toLetters(word){
return Array.from(word.trim());
}
/* =========================================================
4) SONIDO (Web Audio API, sin archivos externos)
========================================================= */
var audioCtx = null;
function getAudioCtx(){
if (!audioCtx){
var AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
return audioCtx;
}
function playTone(freq, startTime, duration, gainPeak){
var ctx = getAudioCtx();
if (!ctx) return;
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.type = "sine";
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.0001, startTime);
gain.gain.exponentialRampToValueAtTime(gainPeak, startTime + 0.03);
gain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(startTime);
osc.stop(startTime + duration + 0.05);
}
function playSuccessSound(){
if (!state.soundOn) return;
var ctx = getAudioCtx();
if (!ctx) return;
var t = ctx.currentTime;
playTone(523.25, t, 0.16, 0.15); // do
playTone(659.25, t + 0.12, 0.16, 0.15); // mi
playTone(783.99, t + 0.24, 0.24, 0.15); // sol
}
function playRetrySound(){
if (!state.soundOn) return;
var ctx = getAudioCtx();
if (!ctx) return;
var t = ctx.currentTime;
playTone(330, t, 0.18, 0.09);
playTone(294, t + 0.14, 0.22, 0.09);
}
function speak(text){
if (!state.soundOn) return;
if (!("speechSynthesis" in window)) return;
try{
window.speechSynthesis.cancel();
var utter = new SpeechSynthesisUtterance(text);
utter.lang = "es-ES";
utter.rate = 0.95;
window.speechSynthesis.speak(utter);
}catch(e){ /* síntesis de voz no disponible: la app sigue funcionando */ }
}
function setSoundOn(on){
state.soundOn = on;
var label = on ? "🔊 Sonido" : "🔇 Sonido";
var labelShort = on ? "🔊" : "🔇";
[$("soundToggleMenu")].forEach(function(b){ if(b){ b.textContent = label; b.setAttribute("aria-pressed", on ? "true":"false"); }});
[$("soundToggleGame"), $("soundToggleCustom")].forEach(function(b){ if(b){ b.textContent = labelShort; b.setAttribute("aria-pressed", on ? "true":"false"); }});
}
/* =========================================================
5) NAVEGACIÓN ENTRE PANTALLAS
========================================================= */
function showScreen(id){
document.querySelectorAll(".screen").forEach(function(s){ s.classList.remove("active"); });
$(id).classList.add("active");
$(id).scrollIntoView({block:"start"});
}
/* =========================================================
6) INICIO DE PARTIDA
========================================================= */
function startBankGame(level){
state.mode = "bank";
state.level = level;
var bank = level === 2 ? WORD_BANK_LEVEL_2 : WORD_BANK_LEVEL_1;
var shuffledBank = shuffleArray(bank);
state.queue = shuffledBank.slice(0, WORDS_PER_GAME);
state.queueIndex = 0;
state.results = [];
showScreen("screen-game");
loadWord(state.queue[0]);
}
function startCustomGame(wordList){
state.mode = "custom";
state.queue = wordList;
state.queueIndex = 0;
state.results = [];
showScreen("screen-game");
loadWord(state.queue[0]);
}
/* =========================================================
7) CARGA DE UNA PALABRA Y RENDERIZADO
========================================================= */
function loadWord(wordText){
state.correctLetters = toLetters(wordText);
var n = state.correctLetters.length;
state.slots = new Array(n).fill(null);
state.trayOrder = shuffleArray(state.correctLetters.map(function(_, i){ return i; }));
// Evitar que, por azar, la bandeja quede ya en el orden correcto
if (n > 1 && state.trayOrder.every(function(v,i){ return v === i; })){
var t = state.trayOrder[0]; state.trayOrder[0] = state.trayOrder[1]; state.trayOrder[1] = t;
}
state.placementHistory = [];
state.attemptsLeft = MAX_ATTEMPTS;
$("feedbackBanner").className = "feedback-banner";
$("feedbackBanner").textContent = "";
$("solutionBox").style.display = "none";
$("btnContinue").style.display = "none";
$("btnCheck").style.display = "";
$("btnCheck").disabled = true;
$("btnClearSlots").style.display = "";
$("btnRemoveLastWord").style.display = "";
updateProgressUI();
updateAttemptsUI();
renderSlots();
renderTray();
announce("Nueva palabra. " + n + " letras para ordenar.");
}
function updateProgressUI(){
var current = state.queueIndex + 1;
var total = state.queue.length;
if (state.mode === "bank"){
$("progressLabel").textContent = "Nivel " + state.level + " · Palabra " + current + " de " + total;
} else {
$("progressLabel").textContent = total > 1 ? ("Mi palabra " + current + " de " + total) : "Mi propia palabra";
}
$("progressFill").style.width = Math.round((state.queueIndex / total) * 100) + "%";
}
function updateAttemptsUI(){
var wrap = $("attemptsIndicator");
wrap.innerHTML = "";
for (var i = 0; i < MAX_ATTEMPTS; i++){
var span = document.createElement("span");
span.className = "heart" + (i < state.attemptsLeft ? "" : " used");
span.textContent = "❤️";
wrap.appendChild(span);
}
wrap.setAttribute("aria-label", "Te quedan " + state.attemptsLeft + " de " + MAX_ATTEMPTS + " oportunidades.");
}
function renderSlots(){
var zone = $("slotsZone");
zone.innerHTML = "";
state.slots.forEach(function(letterIdx, pos){
var slot = document.createElement("div");
slot.className = "slot" + (letterIdx !== null ? " filled" : "");
slot.dataset.pos = pos;
if (letterIdx !== null){
slot.appendChild(makeLetterCard(letterIdx, true));
} else {
slot.textContent = "";
slot.setAttribute("aria-label", "Hueco " + (pos + 1) + " vacío");
}
zone.appendChild(slot);
});
var allFilled = state.slots.every(function(v){ return v !== null; });
$("btnCheck").disabled = !allFilled;
$("btnRemoveLastWord").disabled = state.placementHistory.length === 0;
}
function renderTray(){
var zone = $("trayZone");
zone.innerHTML = "";
state.trayOrder.forEach(function(letterIdx){
zone.appendChild(makeLetterCard(letterIdx, false));
});
}
function makeLetterCard(letterIdx, isPlaced){
var card = document.createElement("div");
card.className = "letter-card" + (isPlaced ? " placed" : "");
card.textContent = state.correctLetters[letterIdx];
card.dataset.letterIndex = letterIdx;
card.tabIndex = 0;
card.setAttribute("role", "button");
card.setAttribute("aria-label", (isPlaced ? "Letra colocada: " : "Letra: ") + state.correctLetters[letterIdx] + (isPlaced ? ". Pulsa para quitarla." : ". Pulsa para colocarla."));
card.addEventListener("pointerdown", function(e){ onLetterPointerDown(e, card); });
card.addEventListener("keydown", function(e){
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar"){
e.preventDefault();
activateLetter(letterIdx, isPlaced);
}
});
return card;
}
/* =========================================================
8) INTERACCIÓN: pulsar/tocar para colocar (alternativa
accesible) + arrastrar con puntero (ratón, dedo o lápiz)
========================================================= */
function activateLetter(letterIdx, isPlaced){
if (isPlaced){
removeFromSlot(letterIdx);
} else {
placeInNextEmptySlot(letterIdx);
}
}
function markPlaced(letterIdx){
state.placementHistory = state.placementHistory.filter(function(v){ return v !== letterIdx; });
state.placementHistory.push(letterIdx);
}
function markUnplaced(letterIdx){
state.placementHistory = state.placementHistory.filter(function(v){ return v !== letterIdx; });
}
function placeInNextEmptySlot(letterIdx){
var pos = state.slots.indexOf(null);
if (pos === -1) return;
state.slots[pos] = letterIdx;
state.trayOrder = state.trayOrder.filter(function(v){ return v !== letterIdx; });
markPlaced(letterIdx);
renderSlots();
renderTray();
announce("Letra " + state.correctLetters[letterIdx] + " colocada en el hueco " + (pos + 1) + ".");
}
function placeInSlot(letterIdx, pos){
var previousAtPos = state.slots[pos];
var sourcePos = state.slots.indexOf(letterIdx); // -1 si venía de la bandeja
if (sourcePos !== -1){
// La letra ya estaba colocada en otro hueco: intercambio real entre los dos huecos
state.slots[sourcePos] = (previousAtPos !== null && previousAtPos !== letterIdx) ? previousAtPos : null;
state.slots[pos] = letterIdx;
markPlaced(letterIdx);
if (previousAtPos !== null && previousAtPos !== letterIdx){
markPlaced(previousAtPos);
}
} else {
// La letra venía de la bandeja de palabras disponibles
state.trayOrder = state.trayOrder.filter(function(v){ return v !== letterIdx; });
state.slots[pos] = letterIdx;
markPlaced(letterIdx);
if (previousAtPos !== null && previousAtPos !== letterIdx){
state.trayOrder.push(previousAtPos);
markUnplaced(previousAtPos);
}
}
renderSlots();
renderTray();
}
function removeFromSlot(letterIdx){
for (var i = 0; i < state.slots.length; i++){
if (state.slots[i] === letterIdx) state.slots[i] = null;
}
if (state.trayOrder.indexOf(letterIdx) === -1) state.trayOrder.push(letterIdx);
markUnplaced(letterIdx);
renderSlots();
renderTray();
announce("Letra " + state.correctLetters[letterIdx] + " ha vuelto a las letras disponibles.");
}
function returnToTray(letterIdx){
var wasPlaced = state.slots.indexOf(letterIdx) !== -1;
for (var i = 0; i < state.slots.length; i++){
if (state.slots[i] === letterIdx) state.slots[i] = null;
}
if (state.trayOrder.indexOf(letterIdx) === -1) state.trayOrder.push(letterIdx);
markUnplaced(letterIdx);
renderSlots();
renderTray();
if (wasPlaced) announce("Letra devuelta a las letras disponibles.");
}
function removeLastPlacedWord(){
if (state.placementHistory.length === 0) return;
var letterIdx = state.placementHistory[state.placementHistory.length - 1];
removeFromSlot(letterIdx);
announce("Se ha quitado la última letra colocada.");
}
function clearSlotHighlights(){
document.querySelectorAll(".slot.slot-hover").forEach(function(s){ s.classList.remove("slot-hover"); });
}
function onLetterPointerDown(e, letterEl){
if (e.button !== undefined && e.button !== 0 && e.pointerType === "mouse") return;
e.preventDefault();
var rect = letterEl.getBoundingClientRect();
dragState = {
letterEl: letterEl,
letterIndex: parseInt(letterEl.dataset.letterIndex, 10),
pointerId: e.pointerId,
startX: e.clientX,
startY: e.clientY,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top,
width: rect.width,
moved: false,
ghost: null
};
try{ letterEl.setPointerCapture(e.pointerId); }catch(err){}
letterEl.addEventListener("pointermove", onLetterPointerMove);
letterEl.addEventListener("pointerup", onLetterPointerUp);
letterEl.addEventListener("pointercancel", onLetterPointerUp);
}
function onLetterPointerMove(e){
if (!dragState) return;
var dx = e.clientX - dragState.startX;
var dy = e.clientY - dragState.startY;
if (!dragState.moved && Math.sqrt(dx*dx + dy*dy) > 10){
dragState.moved = true;
var ghost = dragState.letterEl.cloneNode(true);
ghost.className = "letter-card letter-ghost";
ghost.style.width = dragState.width + "px";
document.body.appendChild(ghost);
dragState.ghost = ghost;
dragState.letterEl.classList.add("dragging-source");
}
if (dragState.moved && dragState.ghost){
dragState.ghost.style.left = (e.clientX - dragState.offsetX) + "px";
dragState.ghost.style.top = (e.clientY - dragState.offsetY) + "px";
clearSlotHighlights();
var target = document.elementFromPoint(e.clientX, e.clientY);
var slot = target && target.closest(".slot");
if (slot) slot.classList.add("slot-hover");
}
}
function onLetterPointerUp(e){
if (!dragState) return;
var letterEl = dragState.letterEl;
var letterIdx = dragState.letterIndex;
letterEl.removeEventListener("pointermove", onLetterPointerMove);
letterEl.removeEventListener("pointerup", onLetterPointerUp);
letterEl.removeEventListener("pointercancel", onLetterPointerUp);
clearSlotHighlights();
if (dragState.moved){
if (dragState.ghost) dragState.ghost.remove();
letterEl.classList.remove("dragging-source");
var target = document.elementFromPoint(e.clientX, e.clientY);
var slot = target && target.closest(".slot");
var trayZone = target && target.closest("#trayZone");
if (slot){
var pos = parseInt(slot.dataset.pos, 10);
placeInSlot(letterIdx, pos);
announce("Letra " + state.correctLetters[letterIdx] + " colocada en el hueco " + (pos + 1) + ".");
} else if (trayZone){
returnToTray(letterIdx);
}
} else {
var isPlaced = state.slots.indexOf(letterIdx) !== -1;
activateLetter(letterIdx, isPlaced);
}
dragState = null;
}
/* =========================================================
9) COMPROBACIÓN DE LA RESPUESTA
========================================================= */
function checkAnswer(){
var isCorrect = state.slots.every(function(letterIdx, pos){
if (letterIdx === null) return false;
var placed = state.correctLetters[letterIdx].toLowerCase();
var expected = state.correctLetters[pos].toLowerCase();
return placed === expected;
});
var attemptNumber = MAX_ATTEMPTS - state.attemptsLeft + 1;
if (isCorrect){
handleCorrectAnswer(attemptNumber);
} else {
handleWrongAnswer();
}
}
function handleCorrectAnswer(attemptNumber){
playSuccessSound();
var msg = pickRandom(SUCCESS_MESSAGES);
var banner = $("feedbackBanner");
banner.className = "feedback-banner success";
banner.textContent = "✅ " + msg;
announce(msg + " Palabra correcta.");
celebrate();
state.results.push({ firstTry: attemptNumber === 1, withHelp: attemptNumber > 1, neededSolution: false });
$("btnCheck").style.display = "none";
$("btnClearSlots").style.display = "none";
$("btnRemoveLastWord").style.display = "none";
$("btnContinue").style.display = "";
$("btnContinue").focus();
}
function handleWrongAnswer(){
state.attemptsLeft -= 1;
updateAttemptsUI();
playRetrySound();
if (state.attemptsLeft > 0){
var msg = pickRandom(RETRY_MESSAGES);
var banner = $("feedbackBanner");
banner.className = "feedback-banner retry";
banner.textContent = "🔁 " + msg + " Te quedan " + state.attemptsLeft + (state.attemptsLeft === 1 ? " oportunidad." : " oportunidades.");
announce(msg);
} else {
state.results.push({ firstTry: false, withHelp: false, neededSolution: true });
var banner2 = $("feedbackBanner");
banner2.className = "feedback-banner info";
banner2.textContent = "💡 Vamos a ver juntos cómo se ordena esta palabra. ¡Lo importante es seguir intentándolo!";
announce("Aquí tienes la palabra ordenada correctamente.");
$("solutionText").textContent = state.correctLetters.join("");
$("solutionBox").style.display = "";
$("btnCheck").style.display = "none";
$("btnClearSlots").style.display = "none";
$("btnRemoveLastWord").style.display = "none";
$("btnContinue").style.display = "";
$("btnContinue").focus();
}
}
function resetCurrentAttemptWords(){
state.trayOrder = shuffleArray(state.correctLetters.map(function(_, i){ return i; }));
state.slots = new Array(state.correctLetters.length).fill(null);
state.placementHistory = [];
renderSlots();
renderTray();
announce("Letras reiniciadas.");
}
/* =========================================================
10) CELEBRACIÓN (estrellas / confeti discretos)
========================================================= */
function celebrate(){
var badge = document.createElement("div");
badge.className = "pop-badge";
badge.setAttribute("aria-hidden", "true");
badge.textContent = "⭐ ¡Genial! ⭐";
document.body.appendChild(badge);
window.setTimeout(function(){ badge.remove(); }, 1200);
if (state.reducedMotion) return; // sin confeti si el usuario prefiere menos movimiento
var layer = $("celebrationLayer");
var emojis = ["⭐","✨","🎉","🌟"];
for (var i = 0; i < 10; i++){
(function(){
var piece = document.createElement("span");
piece.className = "confetto";
piece.textContent = emojis[Math.floor(Math.random()*emojis.length)];
piece.style.left = (Math.random() * 100) + "vw";
piece.style.animationDelay = (Math.random() * 0.3) + "s";
layer.appendChild(piece);
window.setTimeout(function(){ piece.remove(); }, 1800);
})();
}
}
/* =========================================================
11) AVANCE DE PALABRA / FIN DE PARTIDA
========================================================= */
function goToNext(){
state.queueIndex += 1;
if (state.queueIndex >= state.queue.length){
showResults();
} else {
loadWord(state.queue[state.queueIndex]);
}
}
function showResults(){
var firstTry = state.results.filter(function(r){ return r.firstTry; }).length;
var withHelp = state.results.filter(function(r){ return r.withHelp; }).length;
var solutionShown = state.results.filter(function(r){ return r.neededSolution; }).length;
$("statFirstTry").textContent = firstTry;
$("statWithHelp").textContent = withHelp;
$("statSolution").textContent = solutionShown;
var total = state.results.length;
var goodTotal = firstTry + withHelp;
var finalMsg;
if (goodTotal === total){
finalMsg = "¡Muy buen trabajo! Has ordenado las " + total + " palabras.";
} else if (goodTotal >= Math.ceil(total * 0.6)){
finalMsg = "¡Muy buen trabajo! Has conseguido ordenar " + goodTotal + " de " + total + " palabras.";
} else {
finalMsg = "¡Sigue practicando! Cada partida lo haces mejor.";
}
$("finalMessage").textContent = finalMsg;
announce(finalMsg);
if (state.mode === "custom"){
$("btnPlayAgain").textContent = "✏️ Introducir otras palabras";
$("btnPlayAgain").dataset.customFlow = "1";
} else {
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
delete $("btnPlayAgain").dataset.customFlow;
}
showScreen("screen-results");
}
/* =========================================================
12) EVENTOS DE INTERFAZ
========================================================= */
$("btnStartBank").addEventListener("click", function(){ showScreen("screen-level"); });
$("btnLevel1").addEventListener("click", function(){ startBankGame(1); });
$("btnLevel2").addEventListener("click", function(){ startBankGame(2); });
$("btnBackToMenuFromLevel").addEventListener("click", function(){ showScreen("screen-menu"); });
$("btnStartCustom").addEventListener("click", function(){
$("customInput").value = "";
$("customError").classList.remove("show");
showScreen("screen-custom");
window.setTimeout(function(){ $("customInput").focus(); }, 50);
});
$("btnPrepareCustom").addEventListener("click", function(){
var rawLines = $("customInput").value.split("\n");
var words = rawLines
.map(function(line){ return line.trim().toLowerCase(); })
.filter(function(line){ return line.length > 0; });
var allValid = words.length > 0 &&
words.length <= MAX_CUSTOM_WORDS &&
words.every(function(w){ return SPANISH_LETTERS_RE.test(w) && toLetters(w).length >= 3; });
if (!allValid){
$("customError").classList.add("show");
announce("Escribe entre una y cinco palabras, cada una con al menos tres letras y sin espacios.");
return;
}
$("customError").classList.remove("show");
startCustomGame(words);
});
$("btnCheck").addEventListener("click", checkAnswer);
$("btnClearSlots").addEventListener("click", resetCurrentAttemptWords);
$("btnRemoveLastWord").addEventListener("click", removeLastPlacedWord);
$("btnContinue").addEventListener("click", goToNext);
$("btnBackToMenu").addEventListener("click", function(){ showScreen("screen-menu"); });
$("btnBackToMenuFromCustom").addEventListener("click", function(){ showScreen("screen-menu"); });
$("btnBackToMenuFromResults").addEventListener("click", function(){
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
delete $("btnPlayAgain").dataset.customFlow;
showScreen("screen-menu");
});
$("btnPlayAgain").addEventListener("click", function(){
if ($("btnPlayAgain").dataset.customFlow === "1"){
delete $("btnPlayAgain").dataset.customFlow;
$("btnPlayAgain").textContent = "🔁 Jugar de nuevo";
$("customInput").value = "";
$("customError").classList.remove("show");
showScreen("screen-custom");
window.setTimeout(function(){ $("customInput").focus(); }, 50);
} else {
startBankGame(state.level);
}
});
$("btnSpeakInstruction").addEventListener("click", function(){
speak("Ordena las letras para formar la palabra.");
});
function bindSoundToggle(btn){
btn.addEventListener("click", function(){ setSoundOn(!state.soundOn); });
}
[$("soundToggleMenu"), $("soundToggleGame"), $("soundToggleCustom")].forEach(bindSoundToggle);
/* =========================================================
13) INICIALIZACIÓN
========================================================= */
setSoundOn(true);
showScreen("screen-menu");
})();
</script>
</body>
</html>