Mejoras y optimizaciones en general.
This commit is contained in:
@@ -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>
|
||||
|
@@ -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();
|
@@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user