Mejoras y optimizaciones en general.
This commit is contained in:
@@ -3,17 +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 Avanzado</h1>
|
||||
<h1 id="title">Juego de Memoria Avanzado</h1>
|
||||
<p>Haz clic sobre las cartas para descubrir y encontrar las parejas.</p>
|
||||
<div id="hud">
|
||||
<span id="moves"></span>
|
||||
<span id="timer"></span>
|
||||
|
||||
<div id="controls">
|
||||
<label for="difficulty">Dificultad:</label>
|
||||
<select id="difficulty">
|
||||
<option value="facil">Fácil (6 parejas)</option>
|
||||
<option value="normal" selected>Normal (12 parejas)</option>
|
||||
<option value="dificil">Difícil (18 parejas)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="game-board"></div>
|
||||
<div id="status"></div>
|
||||
|
||||
<div id="hud">
|
||||
<span id="moves" aria-live="polite"></span>
|
||||
<span id="timer" aria-live="polite"></span>
|
||||
<span id="best"></span>
|
||||
</div>
|
||||
<div id="game-board" role="grid" aria-labelledby="title"></div>
|
||||
<div id="status" aria-live="polite"></div>
|
||||
<button id="restart-btn">Reiniciar</button>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
@@ -1,67 +1,209 @@
|
||||
const symbols = [
|
||||
'use strict';
|
||||
|
||||
// Símbolos del juego (pool amplio para distintas dificultades)
|
||||
const SYMBOLS = [
|
||||
'🐶','🌸','⚽','🍕','🎲','🌞','🚗','🍩',
|
||||
'⭐','🚀','🎮','💎'
|
||||
'⭐','🚀','🎮','💎','🐱','🍔','🍟','🎧',
|
||||
'🍓','🍍','🥝','🍇','🍒','🍉','🍊','🧩',
|
||||
'🎯','🪙','🧠','🦄','🦊','🦁','🐼','🐸',
|
||||
'🏀','🏐','🎳','🎹','🎻','🥁','🎺','🎷'
|
||||
];
|
||||
let cards = [];
|
||||
|
||||
// Configuración por dificultad
|
||||
const DIFFICULTIES = {
|
||||
facil: { pairs: 6, maxMoves: 40, timeSec: 90 },
|
||||
normal: { pairs: 12, maxMoves: 60, timeSec: 120 },
|
||||
dificil: { pairs: 18, maxMoves: 85, timeSec: 180 }
|
||||
};
|
||||
|
||||
// Estado
|
||||
let deck = []; // símbolos duplicados y mezclados
|
||||
let firstCard = null;
|
||||
let secondCard = null;
|
||||
let lockBoard = false;
|
||||
let matches = 0;
|
||||
let moves = 0;
|
||||
const maxMoves = 45;
|
||||
let timer = 120; // segundos
|
||||
let timer = 0;
|
||||
let timerInterval = null;
|
||||
const boardDiv = document.getElementById("game-board");
|
||||
const statusDiv = document.getElementById("status");
|
||||
const restartBtn = document.getElementById("restart-btn");
|
||||
const movesSpan = document.getElementById("moves");
|
||||
const timerSpan = document.getElementById("timer");
|
||||
let totalPairs = DIFFICULTIES.normal.pairs;
|
||||
let maxMoves = DIFFICULTIES.normal.maxMoves;
|
||||
|
||||
// DOM
|
||||
const boardDiv = document.getElementById('game-board');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const restartBtn = document.getElementById('restart-btn');
|
||||
const movesSpan = document.getElementById('moves');
|
||||
const timerSpan = document.getElementById('timer');
|
||||
const difficultySelect = document.getElementById('difficulty');
|
||||
const bestSpan = document.getElementById('best');
|
||||
|
||||
// Utils
|
||||
function shuffle(array) {
|
||||
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 bestKey(diff) {
|
||||
return `memoryv2_best_${diff}`;
|
||||
}
|
||||
function getBest(diff) {
|
||||
const v = localStorage.getItem(bestKey(diff));
|
||||
return v ? parseInt(v, 10) : null;
|
||||
}
|
||||
function setBestIfBetter(diff, movesUsed, timeLeft) {
|
||||
// Métrica compuesta: menor movimientos prioriza, y mayor tiempo restante también importa
|
||||
// Guardamos mejor (menores movimientos) y desempate por mayor tiempo restante
|
||||
const key = bestKey(diff);
|
||||
const prevRaw = localStorage.getItem(key);
|
||||
let prev = prevRaw ? JSON.parse(prevRaw) : null;
|
||||
const current = { moves: movesUsed, timeLeft: timeLeft };
|
||||
if (!prev || current.moves < prev.moves || (current.moves === prev.moves && current.timeLeft > prev.timeLeft)) {
|
||||
localStorage.setItem(key, JSON.stringify(current));
|
||||
}
|
||||
prev = JSON.parse(localStorage.getItem(key));
|
||||
if (bestSpan) {
|
||||
if (prev) bestSpan.textContent = `Mejor: ${prev.moves} mov · ${prev.timeLeft}s`;
|
||||
else bestSpan.textContent = 'Mejor: —';
|
||||
}
|
||||
}
|
||||
function updateBestDisplay(diff) {
|
||||
const prevRaw = localStorage.getItem(bestKey(diff));
|
||||
if (!bestSpan) return;
|
||||
if (!prevRaw) {
|
||||
bestSpan.textContent = 'Mejor: —';
|
||||
} else {
|
||||
const prev = JSON.parse(prevRaw);
|
||||
bestSpan.textContent = `Mejor: ${prev.moves} mov · ${prev.timeLeft}s`;
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
matches = 0;
|
||||
moves = 0;
|
||||
timer = 120;
|
||||
function updateHUD() {
|
||||
if (movesSpan) movesSpan.textContent = `Movimientos: ${moves} / ${maxMoves}`;
|
||||
if (timerSpan) timerSpan.textContent = `Tiempo: ${timer}s`;
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
}
|
||||
}
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
timerInterval = setInterval(() => {
|
||||
timer--;
|
||||
updateHUD();
|
||||
if (timer <= 0) {
|
||||
endGame(false, '¡Se acabó el tiempo!');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function gridColumnsFor(totalCards) {
|
||||
// Determina columnas para una rejilla compacta
|
||||
if (totalCards <= 12) return 4;
|
||||
if (totalCards <= 16) return 4;
|
||||
if (totalCards <= 24) return 6;
|
||||
if (totalCards <= 36) return 6;
|
||||
return 6;
|
||||
}
|
||||
|
||||
// Render tarjeta
|
||||
function createCard(symbol, idx) {
|
||||
const card = document.createElement('button');
|
||||
card.type = 'button';
|
||||
card.className = 'card';
|
||||
card.dataset.index = String(idx);
|
||||
card.dataset.symbol = symbol;
|
||||
card.textContent = '';
|
||||
card.setAttribute('aria-label', 'Carta');
|
||||
card.setAttribute('aria-disabled', 'false');
|
||||
|
||||
card.addEventListener('click', () => flipCard(card));
|
||||
card.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
flipCard(card);
|
||||
}
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
function resetTurn() {
|
||||
firstCard = null;
|
||||
secondCard = null;
|
||||
lockBoard = false;
|
||||
statusDiv.textContent = '';
|
||||
updateHUD();
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
cards = [...symbols, ...symbols];
|
||||
shuffle(cards);
|
||||
boardDiv.innerHTML = '';
|
||||
cards.forEach((symbol, idx) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.dataset.index = idx;
|
||||
card.dataset.symbol = symbol;
|
||||
card.textContent = '';
|
||||
card.onclick = () => flipCard(card);
|
||||
boardDiv.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function unflip(el) {
|
||||
if (!el) return;
|
||||
el.classList.remove('flipped');
|
||||
el.textContent = '';
|
||||
el.setAttribute('aria-disabled', 'false');
|
||||
}
|
||||
|
||||
// Inicio/reinicio
|
||||
function startGame() {
|
||||
// Leer dificultad
|
||||
const diff = (difficultySelect && difficultySelect.value) || 'normal';
|
||||
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
|
||||
totalPairs = conf.pairs;
|
||||
maxMoves = conf.maxMoves;
|
||||
timer = conf.timeSec;
|
||||
|
||||
// Reiniciar estado
|
||||
matches = 0;
|
||||
moves = 0;
|
||||
firstCard = null;
|
||||
secondCard = null;
|
||||
lockBoard = false;
|
||||
if (statusDiv) statusDiv.textContent = '';
|
||||
updateHUD();
|
||||
updateBestDisplay(diff);
|
||||
|
||||
// Construir deck
|
||||
const chosen = pickSymbols(totalPairs);
|
||||
deck = [...chosen, ...chosen];
|
||||
shuffle(deck);
|
||||
|
||||
// Render tablero
|
||||
boardDiv.textContent = '';
|
||||
const totalCards = deck.length;
|
||||
const cols = gridColumnsFor(totalCards);
|
||||
boardDiv.style.gridTemplateColumns = `repeat(${cols}, minmax(45px, 1fr))`;
|
||||
|
||||
deck.forEach((symbol, idx) => {
|
||||
const card = createCard(symbol, idx);
|
||||
boardDiv.appendChild(card);
|
||||
});
|
||||
|
||||
// Foco inicial y arranque de tiempo
|
||||
const first = boardDiv.querySelector('.card');
|
||||
if (first) first.focus();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// Lógica de flip
|
||||
function flipCard(card) {
|
||||
if (lockBoard) return;
|
||||
if (card.classList.contains('flipped') || card.classList.contains('matched')) return;
|
||||
|
||||
card.classList.add('flipped');
|
||||
card.textContent = card.dataset.symbol;
|
||||
card.setAttribute('aria-disabled', 'true');
|
||||
|
||||
if (!firstCard) {
|
||||
firstCard = card;
|
||||
return; // ¡Esperamos por la segunda carta!
|
||||
return;
|
||||
}
|
||||
|
||||
if (card === firstCard) return;
|
||||
|
||||
secondCard = card;
|
||||
lockBoard = true;
|
||||
moves++;
|
||||
@@ -77,55 +219,40 @@ function flipCard(card) {
|
||||
firstCard.classList.add('hide');
|
||||
secondCard.classList.add('hide');
|
||||
resetTurn();
|
||||
// Verifica victoria DESPUÉS de ocultar
|
||||
if (matches === symbols.length) {
|
||||
clearInterval(timerInterval);
|
||||
if (matches === totalPairs) {
|
||||
// Victoria
|
||||
stopTimer();
|
||||
statusDiv.textContent = `¡Felicidades! Lo lograste en ${moves} movimientos y te sobraron ${timer} segs 🎉`;
|
||||
lockBoard = true; // Deshabilita el tablero tras terminar
|
||||
setBestIfBetter((difficultySelect && difficultySelect.value) || 'normal', moves, timer);
|
||||
lockBoard = true;
|
||||
}
|
||||
}, 800);
|
||||
|
||||
}, 600);
|
||||
} else {
|
||||
// No es PAR
|
||||
setTimeout(() => {
|
||||
firstCard.classList.remove('flipped');
|
||||
secondCard.classList.remove('flipped');
|
||||
firstCard.textContent = '';
|
||||
secondCard.textContent = '';
|
||||
unflip(firstCard);
|
||||
unflip(secondCard);
|
||||
resetTurn();
|
||||
}, 900);
|
||||
}, 700);
|
||||
}
|
||||
|
||||
// DERROTA: por movimientos
|
||||
// Derrota por límite de movimientos
|
||||
if (moves >= maxMoves) {
|
||||
endGame(false, "Has alcanzado el límite de movimientos. ¡Inténtalo otra vez!");
|
||||
endGame(false, 'Has alcanzado el límite de movimientos. ¡Inténtalo otra vez!');
|
||||
}
|
||||
}
|
||||
|
||||
function updateHUD() {
|
||||
movesSpan.textContent = `Movimientos: ${moves} / ${maxMoves}`;
|
||||
timerSpan.textContent = `Tiempo: ${timer}s`;
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
timer--;
|
||||
updateHUD();
|
||||
if (timer <= 0) {
|
||||
endGame(false, "¡Se acabó el tiempo!");
|
||||
}
|
||||
}
|
||||
|
||||
function resetTurn() {
|
||||
firstCard = null;
|
||||
secondCard = null;
|
||||
lockBoard = false;
|
||||
}
|
||||
|
||||
function endGame(win, msg) {
|
||||
lockBoard = true;
|
||||
clearInterval(timerInterval);
|
||||
stopTimer();
|
||||
statusDiv.textContent = msg;
|
||||
}
|
||||
|
||||
restartBtn.onclick = startGame;
|
||||
// Listeners
|
||||
restartBtn.addEventListener('click', startGame);
|
||||
if (difficultySelect) {
|
||||
difficultySelect.addEventListener('change', startGame);
|
||||
}
|
||||
|
||||
// Init
|
||||
startGame();
|
Reference in New Issue
Block a user