UNPKG

number-memory-game-component

Version:

A simple number memory game implemented as a web component.

477 lines (422 loc) 27.4 kB
class NumberMemoryGame extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.randomNumbers = []; this.gameInterval = 3000; this.roundTimerId = null; this.feedbackTimerId = null; this.roundsPlayed = 0; this.totalRoundsPerSet = 5; this.currentScore = 0; this.incorrectAttemptsThisRound = 0; this.maxIncorrectAttemptsPerRound = 2; this.activeInputIndex = 0; // Index for the active guess display slot this._startGame = this._startGame.bind(this); this._submitGuess = this._submitGuess.bind(this); this._resetGameToSettings = this._resetGameToSettings.bind(this); this._handleNumpadClick = this._handleNumpadClick.bind(this); } connectedCallback() { this._render(); this._attachEventListeners(); this._resetGameToSettings(); } disconnectedCallback() { if (this.roundTimerId) clearTimeout(this.roundTimerId); if (this.feedbackTimerId) clearTimeout(this.feedbackTimerId); this.startGameBtn.removeEventListener('click', this._startGame); this.submitGuessBtn.removeEventListener('click', this._submitGuess); this.playAgainBtn.removeEventListener('click', this._resetGameToSettings); this.numpad.removeEventListener('click', this._handleNumpadClick); // No specific listeners on guess display slots to remove, as they don't take direct input } _render() { this.shadowRoot.innerHTML = ` <link href="https://cdn.tailwindcss.com" rel="stylesheet"> <style> :host { display: block; } .game-container { font-family: 'Inter', sans-serif; background-image: linear-gradient(to bottom right, #0f172a, #1e293b); color: white; padding: 1.5rem; } @media (min-width: 768px) { .game-container { padding: 2.5rem; } } .number-display-item { transition: opacity 0.5s ease-in-out; } /* Styling for the new guess display slots */ .guess-display-slot { display: inline-flex; /* To allow width and height */ justify-content: center; align-items: center; width: 30%; /* Similar to w-1/3 */ min-height: 3rem; /* p-3 equivalent height */ padding: 0.75rem; /* p-3 */ text-align: center; font-size: 1.5rem; /* text-2xl */ background-color: #334155; /* bg-slate-700 */ border: 1px solid #475569; /* border-slate-600 */ border-radius: 0.5rem; /* rounded-lg */ color: #ffffff; /* text-white */ transition: border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; box-sizing: border-box; } .guess-display-slot.focused { border-color: #60a5fa; /* Tailwind's blue-400 */ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.5); } .guess-display-slot.border-green-500 { border-color: #22c55e !important; } .guess-display-slot.border-red-500 { border-color: #ef4444 !important; } .message-box { min-height: 5.5rem; } .numpad-btn { width: 100%; padding: 0.75rem; background-color: #334155; border-radius: 0.5rem; font-size: 1.25rem; font-weight: 600; color: white; transition-property: background-color; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } .numpad-btn:hover { background-color: #475569; } .numpad-btn:focus { /* Basic focus for accessibility, can be enhanced */ outline: 2px solid #38bdf8; outline-offset: 2px; } /* Minimal set of direct Tailwind translations for core functionality */ .hidden { display: none; } .flex { display: flex; } .justify-around { justify-content: space-around; } .items-center { align-items: center; } .text-center { text-align: center; } .font-bold { font-weight: 700; } .w-full { width: 100%; } .mt-4 { margin-top: 1rem; } .mb-6 { margin-bottom: 1.5rem; } .mb-1 { margin-bottom: 0.25rem; } .mt-2 { margin-top: 0.5rem; } .my-8 { margin-top: 2rem; margin-bottom: 2rem; } .p-3 { padding: 0.75rem; } .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } .px-4 { padding-left: 1rem; padding-right: 1rem; } .rounded-lg { border-radius: 0.5rem; } .text-sky-400 { color: #38bdf8; } .text-slate-300 { color: #cbd5e1; } .text-slate-400 { color: #94a3b8; } .bg-slate-700 { background-color: #334155; } .border-slate-600 { border-color: #475569; } .bg-sky-500 { background-color: #0ea5e9; } .hover\:bg-sky-600:hover { background-color: #0284c7; } .bg-green-500 { background-color: #22c55e; } .hover\:bg-green-600:hover { background-color: #16a34a; } .bg-red-700 { background-color: #b91c1c; } .hover\:bg-red-600:hover { background-color: #991b1b; } .bg-sky-700 { background-color: #0369a1; } .hover\:bg-sky-600:hover { background-color: #075985; } .grid { display: grid; } .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .gap-2 { gap: 0.5rem; } .text-3xl { font-size: 1.875rem; } .md\:text-4xl { @media (min-width: 768px) { font-size: 2.25rem; } } .text-sm { font-size: 0.875rem; } .font-medium { font-weight: 500; } .min-h-\\[80px\\] { min-height: 80px; } .space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; } .space-x-2 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.5rem; } .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { @media (min-width: 640px) { margin-left: 1rem; } } .shadow-md { box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06); } .hover\:shadow-lg:hover { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); } .transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } .duration-150 { transition-duration: 150ms; } .ease-in-out { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; } .focus\:ring-2:focus { box-shadow: 0 0 0 2px var(--tw-ring-color, #38bdf8); } .focus\:ring-sky-400:focus { --tw-ring-color: #38bdf8; } .focus\:ring-green-400:focus { --tw-ring-color: #4ade80; } .focus\:ring-opacity-75:focus { /* Opacity might need JS if not using full Tailwind */ } .bg-green-500\\/20 { background-color: rgba(34, 197, 94, 0.2); } .text-green-400 { color: #4ade80; } .bg-red-500\\/20 { background-color: rgba(239, 68, 68, 0.2); } .text-red-400 { color: #f87171; } </style> <div class="game-container"> <header class="mb-6 text-center"> <h1 class="text-3xl md:text-4xl font-bold text-sky-400">Provocarea Memoriei Numerelor</h1> <p class="text-slate-400 mt-2">Testează-ți memoria pe termen scurt pe parcursul a 5 runde!</p> </header> <section id="settingsSection" class="mb-6"> <label for="intervalInput" class="block text-sm font-medium text-slate-300 mb-1">Interval (secunde pe rundă):</label> <input type="number" id="intervalInput" value="3" min="1" max="10" class="w-full p-3 bg-slate-700 border border-slate-600 rounded-lg text-white focus:ring-2 focus:ring-sky-500" placeholder="ex: 3"> <button id="startGameBtn" class="mt-4 w-full bg-sky-500 hover:bg-sky-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-opacity-75"> Începe 5 Runde </button> </section> <section id="gameSection" class="hidden"> <div id="numberDisplay" class="flex justify-around items-center my-8 p-4 bg-slate-700 rounded-lg min-h-[80px]"> </div> <div id="guessInputArea" class="hidden mt-6 space-y-4"> <p id="guessPrompt" class="text-slate-300 text-center">Numerele au dispărut! Introdu ce ai reținut:</p> <div class="flex justify-around space-x-2 sm:space-x-4"> <span id="guessDisplay1" data-index="0" class="guess-display-slot" aria-label="Primul număr ghicit">-</span> <span id="guessDisplay2" data-index="1" class="guess-display-slot" aria-label="Al doilea număr ghicit">-</span> <span id="guessDisplay3" data-index="2" class="guess-display-slot" aria-label="Al treilea număr ghicit">-</span> </div> <button id="submitGuessBtn" class="w-full bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75"> Trimite Răspuns </button> <div id="numpad" class="mt-4 grid grid-cols-3 gap-2"> <button class="numpad-btn" data-value="1">1</button> <button class="numpad-btn" data-value="2">2</button> <button class="numpad-btn" data-value="3">3</button> <button class="numpad-btn" data-value="4">4</button> <button class="numpad-btn" data-value="5">5</button> <button class="numpad-btn" data-value="6">6</button> <button class="numpad-btn" data-value="7">7</button> <button class="numpad-btn" data-value="8">8</button> <button class="numpad-btn" data-value="9">9</button> <button class="numpad-btn bg-red-700 hover:bg-red-600" data-value="clear">Șterge</button> <button class="numpad-btn" data-value="0">0</button> <button class="numpad-btn bg-sky-700 hover:bg-sky-600" data-value="next">Următor</button> </div> </div> </section> <div id="messageArea" class="mt-6 text-center message-box"> </div> <button id="playAgainBtn" class="hidden mt-4 w-full bg-sky-500 hover:bg-sky-600 text-white font-semibold py-3 px-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-opacity-75"> Joacă un Set Nou </button> </div> `; this._getDOMReferences(); } _getDOMReferences() { this.intervalInput = this.shadowRoot.getElementById('intervalInput'); this.startGameBtn = this.shadowRoot.getElementById('startGameBtn'); this.settingsSection = this.shadowRoot.getElementById('settingsSection'); this.gameSection = this.shadowRoot.getElementById('gameSection'); this.numberDisplay = this.shadowRoot.getElementById('numberDisplay'); this.guessInputArea = this.shadowRoot.getElementById('guessInputArea'); this.guessPrompt = this.shadowRoot.getElementById('guessPrompt'); // guessInputs acum se referă la elementele <span> this.guessInputs = [ this.shadowRoot.getElementById('guessDisplay1'), this.shadowRoot.getElementById('guessDisplay2'), this.shadowRoot.getElementById('guessDisplay3') ]; this.submitGuessBtn = this.shadowRoot.getElementById('submitGuessBtn'); this.messageArea = this.shadowRoot.getElementById('messageArea'); this.playAgainBtn = this.shadowRoot.getElementById('playAgainBtn'); this.numpad = this.shadowRoot.getElementById('numpad'); } _attachEventListeners() { this.startGameBtn.addEventListener('click', this._startGame); this.submitGuessBtn.addEventListener('click', this._submitGuess); this.playAgainBtn.addEventListener('click', this._resetGameToSettings); this.numpad.addEventListener('click', this._handleNumpadClick); // Adaugă event listeners la sloturile de afișare pentru a le seta ca active la click // Acest lucru îmbunătățește UX-ul dacă utilizatorul dorește să schimbe un slot specific cu numpad-ul this.guessInputs.forEach((slot, index) => { slot.addEventListener('click', () => this._setActiveInput(index)); }); } _setActiveInput(index) { this.guessInputs.forEach(slot => slot.classList.remove('focused')); if (index >= 0 && index < this.guessInputs.length) { this.guessInputs[index].classList.add('focused'); // Nu mai apelăm .focus() deoarece span-urile nu au această metodă în mod implicit this.activeInputIndex = index; } } _generateRandomNumbers() { const numbers = new Set(); while (numbers.size < 3) { numbers.add(Math.floor(Math.random() * 10)); } this.randomNumbers = Array.from(numbers); } _displayNumbers() { this.numberDisplay.innerHTML = this.randomNumbers.map(num => `<span class="text-4xl md:text-5xl font-bold text-sky-400 number-display-item opacity-100">${num}</span>` ).join(''); void this.numberDisplay.offsetWidth; this.numberDisplay.querySelectorAll('.number-display-item').forEach(item => item.classList.add('opacity-100')); } _hideNumbersAndEnableGuessing() { const numberElements = this.numberDisplay.querySelectorAll('.number-display-item'); numberElements.forEach(el => el.classList.remove('opacity-100')); numberElements.forEach(el => el.classList.add('opacity-0')); setTimeout(() => { this.numberDisplay.innerHTML = `<span class="text-slate-400">Le-ai reținut?</span>`; this.guessPrompt.textContent = "Numerele au dispărut! Introdu ce ai reținut:"; this.guessInputArea.classList.remove('hidden'); this.guessInputs.forEach(slot => slot.textContent = '-'); // Placeholder inițial this._setActiveInput(0); this.submitGuessBtn.disabled = false; }, 500); } _startNewRound() { if (this.roundTimerId) clearTimeout(this.roundTimerId); if (this.feedbackTimerId) clearTimeout(this.feedbackTimerId); this.incorrectAttemptsThisRound = 0; this.guessInputArea.classList.add('hidden'); this.submitGuessBtn.disabled = true; this.playAgainBtn.classList.add('hidden'); this.guessInputs.forEach(slot => { slot.classList.remove('border-red-500', 'border-green-500', 'focused'); slot.textContent = '-'; // Resetează la placeholder }); this._showMessage(`Runda ${this.roundsPlayed + 1} din ${this.totalRoundsPerSet}.<br>Pregătește-te să memorezi...`, 'info'); this._generateRandomNumbers(); this._displayNumbers(); this.roundTimerId = setTimeout(() => this._hideNumbersAndEnableGuessing(), this.gameInterval); } _resetGameToSettings() { if (this.roundTimerId) clearTimeout(this.roundTimerId); if (this.feedbackTimerId) clearTimeout(this.feedbackTimerId); this.randomNumbers = []; this.roundsPlayed = 0; this.currentScore = 0; this.incorrectAttemptsThisRound = 0; this.activeInputIndex = 0; this.settingsSection.classList.remove('hidden'); this.gameSection.classList.add('hidden'); this.guessInputArea.classList.add('hidden'); this.playAgainBtn.classList.add('hidden'); this.messageArea.innerHTML = ''; this.numberDisplay.innerHTML = ''; this.startGameBtn.disabled = false; this.startGameBtn.textContent = 'Începe 5 Runde'; this.intervalInput.disabled = false; this.guessInputs.forEach(slot => { slot.textContent = '-'; slot.classList.remove('border-red-500', 'border-green-500', 'focused'); }); } _showMessage(htmlContent, type = 'info') { this.messageArea.innerHTML = `<div class="p-3 rounded-lg ${ type === 'success' ? 'bg-green-500/20 text-green-400' : type === 'error' ? 'bg-red-500/20 text-red-400' : 'bg-slate-700 text-slate-300' }">${htmlContent}</div>`; } _handleRoundEnd(isCorrectGuess) { let roundFeedbackMessage = ''; if (isCorrectGuess) { this.currentScore++; roundFeedbackMessage = `Runda ${this.roundsPlayed + 1}/${this.totalRoundsPerSet}: Corect! <br>Numerele au fost ${this.randomNumbers.join(', ')}.`; this._showMessage(roundFeedbackMessage, 'success'); } else { roundFeedbackMessage = `Runda ${this.roundsPlayed + 1}/${this.totalRoundsPerSet}: Prea multe încercări! <br>Numerele corecte erau ${this.randomNumbers.join(', ')}.`; this._showMessage(roundFeedbackMessage, 'error'); } this.roundsPlayed++; if (this.feedbackTimerId) clearTimeout(this.feedbackTimerId); if (this.roundsPlayed < this.totalRoundsPerSet) { this.feedbackTimerId = setTimeout(() => { this._startNewRound(); }, 3000); } else { let finalMessage = `Set Complet! Scorul Final: ${this.currentScore} din ${this.totalRoundsPerSet} runde corecte.<br>`; if (!isCorrectGuess && this.incorrectAttemptsThisRound >= this.maxIncorrectAttemptsPerRound) { finalMessage += `Ultimele numere (runda ${this.roundsPlayed}) au fost ${this.randomNumbers.join(', ')}.`; } else if (isCorrectGuess) { finalMessage += `Ultimele numere corect ghicite (runda ${this.roundsPlayed}) au fost ${this.randomNumbers.join(', ')}.`; } this._showMessage(finalMessage, this.currentScore >= Math.ceil(this.totalRoundsPerSet / 2) ? 'success' : 'error'); this.playAgainBtn.classList.remove('hidden'); // this.playAgainBtn.focus(); // Button focus is fine this.startGameBtn.textContent = 'Începe 5 Runde'; this.startGameBtn.disabled = true; this.intervalInput.disabled = true; } } _startGame() { const intervalValue = parseInt(this.intervalInput.value, 10); if (isNaN(intervalValue) || intervalValue < 1 || intervalValue > 10) { this._showMessage('Te rog introdu un interval valid (1-10 secunde).', 'error'); // this.intervalInput.focus(); // Input focus is fine return; } this.gameInterval = intervalValue * 1000; this.roundsPlayed = 0; this.currentScore = 0; this.settingsSection.classList.add('hidden'); this.gameSection.classList.remove('hidden'); this.startGameBtn.disabled = true; this.startGameBtn.textContent = 'Set În Desfășurare...'; this.intervalInput.disabled = true; this._startNewRound(); } _submitGuess() { // Citim valorile din textContent-ul span-urilor const userGuesses = this.guessInputs.map(slot => { const val = slot.textContent; // Dacă slotul conține placeholder-ul '-', considerăm NaN return (val === '' || val === '-') ? NaN : parseInt(val, 10); }); if (userGuesses.some(isNaN)) { this._showMessage('Te rog completează toate căsuțele folosind tastatura numerică.', 'error'); return; } this.submitGuessBtn.disabled = true; let allCorrectInRound = true; for (let i = 0; i < 3; i++) { this.guessInputs[i].classList.remove('border-red-500', 'border-green-500'); if (userGuesses[i] === this.randomNumbers[i]) { this.guessInputs[i].classList.add('border-green-500'); } else { allCorrectInRound = false; this.guessInputs[i].classList.add('border-red-500'); } } if (allCorrectInRound) { this._handleRoundEnd(true); } else { this.incorrectAttemptsThisRound++; if (this.incorrectAttemptsThisRound < this.maxIncorrectAttemptsPerRound) { this._showMessage(`Încercarea ${this.incorrectAttemptsThisRound}/${this.maxIncorrectAttemptsPerRound} greșită. Mai încearcă!<br>Numerele introduse: ${userGuesses.map(g => isNaN(g) ? '-' : g).join(', ')}`, 'error'); this.guessPrompt.textContent = `Mai încearcă! (Încercarea ${this.incorrectAttemptsThisRound + 1}/${this.maxIncorrectAttemptsPerRound})`; this.submitGuessBtn.disabled = false; this._setActiveInput(0); } else { this._handleRoundEnd(false); } } } _handleNumpadClick(event) { const target = event.target.closest('button'); if (!target) return; const value = target.dataset.value; const currentSlot = this.guessInputs[this.activeInputIndex]; if (value === 'clear') { currentSlot.textContent = '-'; // Resetează la placeholder // Nu este nevoie de .focus() pe span } else if (value === 'next') { let nextIndex = (this.activeInputIndex + 1) % this.guessInputs.length; this._setActiveInput(nextIndex); } else if (value) { // Este o cifră (0-9) currentSlot.textContent = value; // Setează textContent-ul span-ului if (this.activeInputIndex < this.guessInputs.length - 1) { this._setActiveInput(this.activeInputIndex + 1); } else { // Rămâne pe ultimul slot, dar este actualizat this._setActiveInput(this.activeInputIndex); // Reaplică clasa focused } } } } customElements.define('number-memory-game', NumberMemoryGame);