Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions PR_UPDATE.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 5 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ <h1 class="title">Vault‑Tec Memory</h1>
Moves: <span id="remaining-moves"></span> remaining
</div>

<div class="controls" style="margin-top:1.2rem">
<button class="btn" id="restart">Restart</button>
<button class="btn ghost" id="hint">Reveal 1 Pair</button>
</div>
<div class="controls">
<button class="btn" id="restart">Restart</button>
<button class="btn ghost" id="hint">Reveal 1 Pair</button>
<button class="btn ghost" id="shuffleBoard" disabled>Shuffle Board</button>
</div>

<div class="hint" style="margin-top:1rem;padding:0.8rem;background:rgba(137, 166, 207, 0.1);border-radius:4px">
Tip: Try to remember positions — the Pip‑Boy rewards patience.
Expand Down
139 changes: 125 additions & 14 deletions src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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 {
Expand Down
76 changes: 76 additions & 0 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -73,6 +74,7 @@
let moveLimitEnabled = false;
let moveLimitMax = 0;
let remainingMoves = 0;
let shuffleUsed = false;

// toggle listener
moveLimitToggle.addEventListener('change', () => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down