diff --git a/PR_UPDATE.md b/PR_UPDATE.md new file mode 100644 index 0000000..4c07126 --- /dev/null +++ b/PR_UPDATE.md @@ -0,0 +1,32 @@ +# 🔀 Shuffle Board Power-Up - PR Update + +## ✅ Conflict Resolution Complete + +All Git conflicts have been resolved and the PR is ready for merge. + +## 🎮 Feature Summary + +### Shuffle Board Power-Up +- **One-time use per game** - Button disables after use +- **Smart shuffling** - Only reshuffles unmatched cards, preserves completed pairs +- **Move penalty** - Adds 2 moves to score (affects star rating) +- **Smooth animation** - Bounce effect with staggered card timing +- **Audio feedback** - Rising tone sequence + success sound +- **Responsive design** - Full button text visibility on all screen sizes + +### Technical Implementation +- **HTML**: Added shuffle button to controls section +- **JavaScript**: Complete shuffle logic with state management +- **CSS**: Animation keyframes and responsive button layout +- **Accessibility**: Proper ARIA labels and keyboard support + +### Files Modified +- `index.html` - Added shuffle button +- `src/js/app.js` - Shuffle functionality and game logic +- `src/css/styles.css` - Animation and responsive styling + +## 🚀 Ready for Merge +- ✅ No conflicts +- ✅ All features tested +- ✅ Responsive design verified +- ✅ Clean commit history \ No newline at end of file diff --git a/index.html b/index.html index 80213c4..e823bd1 100644 --- a/index.html +++ b/index.html @@ -72,10 +72,11 @@

Vault‑Tec Memory

Moves: remaining -
- - -
+
+ + + +
Tip: Try to remember positions — the Pip‑Boy rewards patience. diff --git a/src/css/styles.css b/src/css/styles.css index 657fee9..8c0be81 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -262,9 +262,36 @@ h1.title { } .controls { - display: flex; - gap: 0.6rem; - margin-top: 1rem; + display: flex; + gap: 0.6rem; + margin-top: 1rem; + flex-wrap: wrap; +} + +.controls .btn { + flex: 1; + min-width: 120px; + white-space: nowrap; + overflow: visible; + text-overflow: clip; + font-size: 0.85rem; +} + +/* Shuffle button styling */ +#shuffleBoard { + min-width: 120px; + text-align: center; +} + +#shuffleBoard:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#shuffleBoard:not(:disabled):hover { + background: linear-gradient(90deg, rgba(255, 210, 63, 0.1), rgba(255, 210, 63, 0.05)); + border-color: rgba(255, 210, 63, 0.3); + color: var(--vault-yellow); } .btn.ghost#playDemo { @@ -635,6 +662,30 @@ h1.title { } } +/* Shuffle animation for cards */ +.card.shuffling { + animation: shuffleMove 0.8s ease-in-out; + z-index: 10; +} + +@keyframes shuffleMove { + 0% { + transform: translateY(0) scale(1) rotateZ(0deg); + } + 25% { + transform: translateY(-20px) scale(0.9) rotateZ(-5deg); + } + 50% { + transform: translateY(-30px) scale(0.8) rotateZ(5deg); + } + 75% { + transform: translateY(-20px) scale(0.9) rotateZ(-2deg); + } + 100% { + transform: translateY(0) scale(1) rotateZ(0deg); + } +} + .modal-overlay { position: fixed; inset: 0; @@ -760,26 +811,27 @@ h1.title { font-size: 0.75rem; } + @media (max-width: 900px) { body { - padding: 1rem; + padding: 1rem; } .app { - grid-template-columns: 1fr; + grid-template-columns: 1fr; max-width: 920px; - margin: 20px auto; + margin: 20px auto; } .panel { - order: 2; + order: 2; min-height: auto; } .game { order: 1; } .deck { - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 12px; - padding: 0.5rem; + padding: 0.5rem; max-width: 100%; } .card { @@ -794,11 +846,55 @@ h1.title { margin: 2rem auto 0; padding: 1rem 0; } - .star { - width: 34px; - height: 28px; + .star { width: 34px; height: 28px; } + + /* Better button responsiveness for medium screens */ + .controls { + gap: 0.4rem; + flex-wrap: wrap; + } + .controls .btn { + font-size: 0.8rem; + padding: 0.5rem 0.6rem; + min-width: 100px; + flex: 1 1 auto; + } +} + +@media (max-width: 600px) { + .controls { + flex-direction: column; + gap: 0.5rem; + } + .controls .btn { + width: 100%; + flex: none; + font-size: 0.9rem; + padding: 0.6rem; + text-align: center; + min-width: auto; + white-space: normal; + overflow: visible; + } +} +/* Specific breakpoint for button text visibility */ +@media (max-width: 380px) { + .controls { + flex-direction: column; + gap: 0.5rem; + } + .controls .btn { + width: 100%; + flex: none; + min-width: auto; + font-size: 0.9rem; + padding: 0.6rem; + white-space: normal; + overflow: visible; + text-overflow: clip; } } + @media (max-width: 480px) { .deck { grid-template-columns: repeat(3, 1fr); @@ -808,13 +904,19 @@ h1.title { width: 100%; } .panel { - padding: 1rem; + padding: 1rem; } .controls { flex-direction: column; + gap: 0.5rem; } - .btn { + .controls .btn { width: 100%; + flex: none; + font-size: 0.9rem; + padding: 0.6rem; + text-align: center; + white-space: normal; } .footer { padding: 1rem; @@ -826,6 +928,15 @@ h1.title { gap: 0.3rem; } } + +/* Ensure buttons don't get too small on narrow screens */ +@media (max-width: 320px) { + .controls .btn { + font-size: 0.8rem; + padding: 0.5rem; + min-height: 40px; + } +} /* Responsive layout adjustments */ @media (max-width: 1400px) { .app { diff --git a/src/js/app.js b/src/js/app.js index 113abda..e1c89b5 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -29,6 +29,7 @@ const restartBtn = document.getElementById('restart'); const restartBottom = document.getElementById('restart-bottom'); const hintBtn = document.getElementById('hint'); + const shuffleBoardBtn = document.getElementById('shuffleBoard'); const modal = document.getElementById('modalOverlay'); const playAgainBtn = document.getElementById('playAgain'); const closeModalBtn = document.getElementById('closeModal'); @@ -73,6 +74,7 @@ let moveLimitEnabled = false; let moveLimitMax = 0; let remainingMoves = 0; + let shuffleUsed = false; // toggle listener moveLimitToggle.addEventListener('change', () => { @@ -302,10 +304,14 @@ matched = 0; moves = 0; starCount = 3; + shuffleUsed = false; updateUI(); started = false; // enable hint based on difficulty hintBtn.style.display = difficulties[selectedDifficulty].hideHint ? 'none' : ''; + // enable shuffle button for new game + shuffleBoardBtn.disabled = false; + shuffleBoardBtn.textContent = 'Shuffle Board'; // ensure cards are interactive Array.from(deckEl.querySelectorAll('.card')).forEach(c => { c.removeAttribute('aria-disabled'); c.classList.remove('matched', 'is-flip'); }); locked = false; @@ -499,6 +505,76 @@ setTimeout(() => { c1.classList.remove('is-flip'); c2.classList.remove('is-flip'); playTone(440, 0.08); }, 900); }); + // shuffle board power-up + shuffleBoardBtn.addEventListener('click', () => { + if (shuffleUsed || locked) return; + + // Get all unmatched cards + const unmatchedCards = Array.from(deckEl.querySelectorAll('.card:not(.matched)')); + if (unmatchedCards.length < 2) return; + + // Prevent interactions during shuffle + locked = true; + shuffleUsed = true; + shuffleBoardBtn.disabled = true; + shuffleBoardBtn.textContent = 'Used'; + + // Flip all unmatched cards face down first + unmatchedCards.forEach(card => { + if (card.classList.contains('is-flip')) { + card.classList.remove('is-flip'); + } + }); + + // Clear opened cards array + opened = []; + + // Add shuffle animation to all unmatched cards + unmatchedCards.forEach((card, index) => { + setTimeout(() => { + card.classList.add('shuffling'); + playTone(300 + (index * 20), 0.05); // Rising tone sequence + }, index * 50); + }); + + // Shuffle the card positions after animation starts + setTimeout(() => { + // Get current data-src values of unmatched cards + const cardData = unmatchedCards.map(card => card.getAttribute('data-src')); + + // Shuffle the data array + const shuffledData = shuffle([...cardData]); + + // Reassign shuffled data to cards + unmatchedCards.forEach((card, index) => { + card.setAttribute('data-src', shuffledData[index]); + const altText = shuffledData[index].split('.').slice(0, -1).join('.'); + card.setAttribute('aria-label', `Card: ${altText}`); + + // Update the image in the card-front + const img = card.querySelector('.card-front img'); + if (img) { + img.src = `img/${shuffledData[index]}`; + img.alt = altText; + } + }); + + // Deduct points for using the power-up + moves += 2; + updateUI(); + + playTone(800, 0.15); // Success tone + }, 400); + + // Remove shuffle animation and unlock after animation completes + setTimeout(() => { + unmatchedCards.forEach(card => { + card.classList.remove('shuffling'); + }); + locked = false; + }, 800); + }); + // restart & modal handlers restartBtn.addEventListener('click', resetGame); restartBottom.addEventListener('click', resetGame);