Mejoras y optimizaciones en general.

This commit is contained in:
2025-10-03 00:05:08 +02:00
parent bd76741bd2
commit d1a7442ffa
32 changed files with 3336 additions and 783 deletions

View File

@@ -3,13 +3,29 @@
<head>
<meta charset="UTF-8">
<title>Juego de Memoria</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Juego de Memoria</h1>
<h1 id="title">Juego de Memoria</h1>
<p>Haz clic en las cartas para darles la vuelta y encuentra las parejas.</p>
<div id="game-board"></div>
<div id="info"></div>
<div id="controls">
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="facil">Fácil (6 parejas)</option>
<option value="normal" selected>Normal (8 parejas)</option>
<option value="dificil">Difícil (12 parejas)</option>
<option value="extremo">Extremo (18 parejas)</option>
</select>
</div>
<div id="hud">
Movimientos: <span id="moves">0</span> · Tiempo: <span id="time">0s</span> · Mejor: <span id="best-score"></span>
</div>
<div id="game-board" role="grid" aria-labelledby="title"></div>
<div id="info" aria-live="polite"></div>
<button id="reset-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,85 +1,228 @@
const symbols = ['🍎','🍌','🍒','🍇','🍉','🍑','🍊','🍓'];
let cards = [];
'use strict';
// Símbolos disponibles (>= 18 únicos para niveles altos)
const SYMBOLS = [
'🍎','🍌','🍒','🍇','🍉','🍑','🍊','🍓',
'🥝','🍍','🥥','🍐','🍈','🍏','🍔','🍕',
'🎈','⭐','🌙','☀️','⚽','🏀','🎲','🎵',
'🐶','🐱','🐼','🐸','🦊','🦁','🦄','🐯',
'🚗','🚀','✈️','🚲','🏝️','🏰'
];
const DIFFICULTIES = {
facil: { pairs: 6 },
normal: { pairs: 8 },
dificil: { pairs: 12 },
extremo: { pairs: 18 }
};
// Estado del juego
let deck = []; // array de símbolos duplicados y mezclados
let firstCard = null;
let secondCard = null;
let lockBoard = false;
let matches = 0;
let moves = 0;
let totalPairs = DIFFICULTIES.normal.pairs;
let timerId = null;
let startTime = 0;
// DOM
const gameBoard = document.getElementById('game-board');
const infoDiv = document.getElementById('info');
const resetBtn = document.getElementById('reset-btn');
const difficultySelect = document.getElementById('difficulty');
const movesSpan = document.getElementById('moves');
const timeSpan = document.getElementById('time');
const bestSpan = document.getElementById('best-score');
// Utils
function shuffle(array) {
// Fisher-Yates shuffle
for(let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i+1));
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function pickSymbols(n) {
const pool = SYMBOLS.slice();
shuffle(pool);
return pool.slice(0, n);
}
function formatTime(seconds) {
const s = Math.max(0, Math.floor(seconds));
return `${s}s`;
}
function bestKey(diff) {
return `memory_best_time_${diff}`;
}
function getBest(diff) {
const v = localStorage.getItem(bestKey(diff));
return v ? parseInt(v, 10) : null;
}
function setBestIfBetter(diff, timeSec) {
const prev = getBest(diff);
if (prev === null || timeSec < prev) {
localStorage.setItem(bestKey(diff), String(timeSec));
}
const best = getBest(diff);
if (bestSpan) bestSpan.textContent = best !== null ? `${best}s` : '—';
}
function startTimer() {
stopTimer();
startTime = Date.now();
timerId = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
if (timeSpan) timeSpan.textContent = formatTime(elapsed);
}, 250);
}
function stopTimer() {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
}
function gridColumnsFor(totalCards) {
// Aproxima columnas en función del total para formar rejilla compacta
if (totalCards <= 12) return 4;
if (totalCards <= 16) return 4;
if (totalCards <= 24) return 6;
if (totalCards <= 30) return 6;
return 6;
}
function setupBoard() {
// Leer dificultad
const diff = (difficultySelect && difficultySelect.value) || 'normal';
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
totalPairs = conf.pairs;
// Reiniciar estado
matches = 0;
moves = 0;
firstCard = null;
secondCard = null;
lockBoard = false;
infoDiv.textContent = '';
// Duplica los símbolos y los mezcla
cards = [...symbols, ...symbols];
shuffle(cards);
gameBoard.innerHTML = '';
cards.forEach((symbol, idx) => {
const cardEl = document.createElement('div');
if (movesSpan) movesSpan.textContent = String(moves);
if (infoDiv) infoDiv.textContent = '';
// Construir mazo
const chosen = pickSymbols(totalPairs);
deck = [...chosen, ...chosen];
shuffle(deck);
// Render tablero accesible
gameBoard.textContent = '';
const totalCards = deck.length;
const cols = gridColumnsFor(totalCards);
gameBoard.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
deck.forEach((symbol, idx) => {
const cardEl = document.createElement('button');
cardEl.type = 'button';
cardEl.className = 'card';
cardEl.dataset.index = idx;
cardEl.dataset.index = String(idx);
cardEl.dataset.symbol = symbol;
cardEl.textContent = '';
cardEl.onclick = () => flipCard(cardEl);
cardEl.setAttribute('role', 'gridcell');
cardEl.setAttribute('aria-label', 'Carta de memoria');
cardEl.setAttribute('aria-disabled', 'false');
cardEl.addEventListener('click', () => flipCard(cardEl));
cardEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
flipCard(cardEl);
}
});
gameBoard.appendChild(cardEl);
});
// Mostrar mejor tiempo
const best = getBest(diff);
if (bestSpan) bestSpan.textContent = best !== null ? `${best}s` : '—';
// Arrancar temporizador
startTimer();
// Foco inicial
const first = gameBoard.querySelector('.card');
if (first) first.focus();
}
function flipCard(cardEl) {
if (lockBoard) return;
if (cardEl.classList.contains('flipped') || cardEl.classList.contains('matched')) return;
// Voltear carta
cardEl.classList.add('flipped');
cardEl.textContent = cardEl.dataset.symbol;
cardEl.setAttribute('aria-disabled', 'true');
if (!firstCard) {
firstCard = cardEl;
} else if(!secondCard && cardEl !== firstCard) {
return;
}
if (!secondCard && cardEl !== firstCard) {
secondCard = cardEl;
lockBoard = true;
moves++;
if (movesSpan) movesSpan.textContent = String(moves);
if (firstCard.dataset.symbol === secondCard.dataset.symbol) {
// ¡Es pareja!
// Pareja encontrada
firstCard.classList.add('matched');
secondCard.classList.add('matched');
matches++;
resetFlipped(700);
if (matches === symbols.length) {
infoDiv.textContent = '¡Felicidades! Has encontrado todas las parejas 🎉';
}
} else {
// No es pareja, voltea las cartas después de un momento
setTimeout(() => {
firstCard.classList.remove('flipped');
secondCard.classList.remove('flipped');
firstCard.textContent = '';
secondCard.textContent = '';
resetFlipped(0);
}, 900);
resetSelection();
if (matches === totalPairs) {
// Fin de partida
const elapsed = Math.floor((Date.now() - startTime) / 1000);
if (infoDiv) infoDiv.textContent = `¡Felicidades! 🎉 Parejas: ${totalPairs} · Movimientos: ${moves} · Tiempo: ${elapsed}s`;
stopTimer();
const diff = (difficultySelect && difficultySelect.value) || 'normal';
setBestIfBetter(diff, elapsed);
}
}, 400);
} else {
// No es pareja, desvoltear tras un momento
setTimeout(() => {
unflip(firstCard);
unflip(secondCard);
resetSelection();
}, 700);
}
}
}
function resetFlipped(delay) {
setTimeout(() => {
firstCard = null;
secondCard = null;
lockBoard = false;
}, delay);
function unflip(el) {
if (!el) return;
el.classList.remove('flipped');
el.textContent = '';
el.setAttribute('aria-disabled', 'false');
}
resetBtn.onclick = setupBoard;
function resetSelection() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
// Listeners
resetBtn.addEventListener('click', () => {
stopTimer();
setupBoard();
});
if (difficultySelect) {
difficultySelect.addEventListener('change', () => {
stopTimer();
setupBoard();
});
}
// Init
setupBoard();

View File

@@ -143,4 +143,85 @@ h1 {
.card { font-size: clamp(1rem, 20vw, 2rem); }
}
/* ::::::::::::::::::::::::::: */
/* ::::::::::::::::::::::::::: */
/* Controles y HUD */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
#controls select#difficulty {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#hud {
font-size: 1rem;
color: #364f6b;
margin: 0.4rem 0 0.8rem 0;
}
/* Accesibilidad: foco visible y estado deshabilitado */
.card {
outline: none;
border: none;
}
.card:focus-visible {
box-shadow: 0 0 0 4px rgba(252, 81, 133, 0.35);
}
.card[aria-disabled="true"] {
cursor: default;
}
/* Ajustes de tablero para rejillas dinámicas desde JS */
#game-board {
/* JS establecerá grid-template-columns dinámicamente */
}
/* Modo oscuro mejorado */
@media (prefers-color-scheme: dark) {
body {
background: #0f1222;
color: #eaeaf0;
}
h1, #info {
color: #eaeaf0;
}
#hud {
color: #a0a3b0;
}
#controls select#difficulty {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
#game-board {
/* sin cambios, deja que JS decida columnas */
}
.card {
background: #20233a;
color: #eaeaf0;
box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12);
}
.card.flipped {
background: #3d5afe;
color: #fff;
}
.card.matched {
background: #172441;
color: #fff;
opacity: 0.8;
}
#reset-btn {
background: #3d5afe;
}
#reset-btn:hover {
background: #0a2459;
}
.card:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
}