258 lines
6.9 KiB
JavaScript
258 lines
6.9 KiB
JavaScript
'use strict';
|
|
|
|
// Símbolos del juego (pool amplio para distintas dificultades)
|
|
const SYMBOLS = [
|
|
'🐶','🌸','⚽','🍕','🎲','🌞','🚗','🍩',
|
|
'⭐','🚀','🎮','💎','🐱','🍔','🍟','🎧',
|
|
'🍓','🍍','🥝','🍇','🍒','🍉','🍊','🧩',
|
|
'🎯','🪙','🧠','🦄','🦊','🦁','🐼','🐸',
|
|
'🏀','🏐','🎳','🎹','🎻','🥁','🎺','🎷'
|
|
];
|
|
|
|
// 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;
|
|
let timer = 0;
|
|
let timerInterval = null;
|
|
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));
|
|
[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 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
if (card === firstCard) return;
|
|
|
|
secondCard = card;
|
|
lockBoard = true;
|
|
moves++;
|
|
updateHUD();
|
|
|
|
if (firstCard.dataset.symbol === secondCard.dataset.symbol) {
|
|
// Es PAR
|
|
firstCard.classList.add('matched');
|
|
secondCard.classList.add('matched');
|
|
matches++;
|
|
|
|
setTimeout(() => {
|
|
firstCard.classList.add('hide');
|
|
secondCard.classList.add('hide');
|
|
resetTurn();
|
|
if (matches === totalPairs) {
|
|
// Victoria
|
|
stopTimer();
|
|
statusDiv.textContent = `¡Felicidades! Lo lograste en ${moves} movimientos y te sobraron ${timer} segs 🎉`;
|
|
setBestIfBetter((difficultySelect && difficultySelect.value) || 'normal', moves, timer);
|
|
lockBoard = true;
|
|
}
|
|
}, 600);
|
|
} else {
|
|
// No es PAR
|
|
setTimeout(() => {
|
|
unflip(firstCard);
|
|
unflip(secondCard);
|
|
resetTurn();
|
|
}, 700);
|
|
}
|
|
|
|
// Derrota por límite de movimientos
|
|
if (moves >= maxMoves) {
|
|
endGame(false, 'Has alcanzado el límite de movimientos. ¡Inténtalo otra vez!');
|
|
}
|
|
}
|
|
|
|
function endGame(win, msg) {
|
|
lockBoard = true;
|
|
stopTimer();
|
|
statusDiv.textContent = msg;
|
|
}
|
|
|
|
// Listeners
|
|
restartBtn.addEventListener('click', startGame);
|
|
if (difficultySelect) {
|
|
difficultySelect.addEventListener('change', startGame);
|
|
}
|
|
|
|
// Init
|
|
startGame(); |