Skip to content
Merged
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
3 changes: 2 additions & 1 deletion projects/whack-a-mole/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ <h1>Whack-a-Mole</h1>
<button id="start">Start</button>
<span id="timer">30s</span>
<span id="score">Score: 0</span>
<span id="lives">Lives: 3</span>
</div>

<div id="grid" class="grid" role="grid" aria-label="Whack-a-Mole game area"></div>

<p class="notes">Features: Difficulty levels, timed rounds, sounds, and accessibility.</p>
<p class="notes">Features: Difficulty levels, timed rounds, power-ups, obstacles, sounds, and accessibility.</p>
</main>

<script type="module" src="./main.js"></script>
Expand Down
220 changes: 213 additions & 7 deletions projects/whack-a-mole/main.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
const grid = document.getElementById('grid');
const scoreEl = document.getElementById('score');
const timerEl = document.getElementById('timer');
const livesEl = document.getElementById('lives');
const startBtn = document.getElementById('start');
const difficultyEl = document.getElementById('difficulty');

let score = 0, hole = -1, timer, roundTime, moleTimer, timeLeft;
let lives = 3, powerUpActive = null, powerUpEndTime = 0, frozenTime = 0;
let powerUpHole = -1, obstacleHole = -1, currentPowerUp = null, currentObstacle = null;
let powerUpSpawnTimer, obstacleSpawnTimer;

// sound effects
const popSound = new Audio('https://actions.google.com/sounds/v1/cartoon/wood_plank_flicks.ogg');
const hitSound = new Audio('https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg');
const powerUpSound = new Audio('https://actions.google.com/sounds/v1/cartoon/magic_chime_2.ogg');
const bombSound = new Audio('https://actions.google.com/sounds/v1/cartoon/explosion_8.ogg');

// power-up and obstacle types
const POWER_UPS = {
DOUBLE_POINTS: { icon: '⚡', name: 'Double Points', color: '#fbbf24', duration: 5000 },
FREEZE_TIME: { icon: '🕒', name: 'Freeze Time', color: '#3b82f6', duration: 3000 },
EXTRA_LIFE: { icon: '💖', name: 'Extra Life', color: '#ef4444', duration: 0 }
};

const OBSTACLES = {
BOMB: { icon: '💣', name: 'Bomb', color: '#dc2626', penalty: { points: 50, time: 5 } }
};

// difficulty speeds
const speeds = {
Expand All @@ -21,9 +38,33 @@ function render() {
grid.innerHTML = '';
for (let i = 0; i < 9; i++) {
const b = document.createElement('button');
b.className = 'hole' + (i === hole ? ' up' : '');
b.setAttribute('aria-label', i === hole ? 'Mole! Hit it!' : 'Empty hole');
let className = 'hole';
let ariaLabel = 'Empty hole';

if (i === hole) {
className += ' up';
ariaLabel = 'Mole! Hit it!';
} else if (i === powerUpHole) {
className += ' power-up';
ariaLabel = `${currentPowerUp.name}! Click to collect!`;
} else if (i === obstacleHole) {
className += ' obstacle';
ariaLabel = `${currentObstacle.name}! Avoid it!`;
}

b.className = className;
b.setAttribute('aria-label', ariaLabel);
b.addEventListener('click', () => hit(i));

// Add content for power-ups and obstacles
if (i === powerUpHole) {
b.textContent = currentPowerUp.icon;
b.style.backgroundColor = currentPowerUp.color;
} else if (i === obstacleHole) {
b.textContent = currentObstacle.icon;
b.style.backgroundColor = currentObstacle.color;
}

grid.appendChild(b);
}
}
Expand All @@ -35,44 +76,209 @@ function randomHole() {
popSound.play();
}

function spawnPowerUp() {
if (Math.random() < 0.15) { // 15% chance to spawn power-up
const powerUpTypes = Object.keys(POWER_UPS);
const randomType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
currentPowerUp = POWER_UPS[randomType];

// Find empty hole
do {
powerUpHole = Math.floor(Math.random() * 9);
} while (powerUpHole === hole || powerUpHole === obstacleHole);

render();
powerUpSound.currentTime = 0;
powerUpSound.play();

// Auto-hide after 3 seconds
setTimeout(() => {
if (powerUpHole !== -1) {
powerUpHole = -1;
currentPowerUp = null;
render();
}
}, 3000);
}
}

function spawnObstacle() {
if (Math.random() < 0.1) { // 10% chance to spawn obstacle
currentObstacle = OBSTACLES.BOMB;

// Find empty hole
do {
obstacleHole = Math.floor(Math.random() * 9);
} while (obstacleHole === hole || obstacleHole === powerUpHole);

render();

// Auto-hide after 2 seconds
setTimeout(() => {
if (obstacleHole !== -1) {
obstacleHole = -1;
currentObstacle = null;
render();
}
}, 2000);
}
}

function hit(i) {
if (i === hole) {
score++;
// Calculate score with power-up multiplier
let points = 1;
if (powerUpActive === 'DOUBLE_POINTS' && Date.now() < powerUpEndTime) {
points = 2;
}

score += points;
scoreEl.textContent = `Score: ${score}`;
hitSound.currentTime = 0;
hitSound.play();
hole = -1;
render();
} else if (i === powerUpHole) {
// Collect power-up
activatePowerUp(currentPowerUp);
powerUpHole = -1;
currentPowerUp = null;
render();
} else if (i === obstacleHole) {
// Hit obstacle - apply penalty
applyObstaclePenalty(currentObstacle);
obstacleHole = -1;
currentObstacle = null;
render();
}
}

function activatePowerUp(powerUp) {
powerUpSound.currentTime = 0;
powerUpSound.play();

if (powerUp.name === 'Double Points') {
powerUpActive = 'DOUBLE_POINTS';
powerUpEndTime = Date.now() + powerUp.duration;
showNotification('⚡ Double Points Active!');

setTimeout(() => {
powerUpActive = null;
showNotification('⚡ Double Points Ended');
}, powerUp.duration);
} else if (powerUp.name === 'Freeze Time') {
frozenTime = powerUp.duration;
showNotification('🕒 Time Frozen!');

setTimeout(() => {
frozenTime = 0;
showNotification('🕒 Time Unfrozen');
}, powerUp.duration);
} else if (powerUp.name === 'Extra Life') {
lives++;
livesEl.textContent = `Lives: ${lives}`;
showNotification('💖 Extra Life Gained!');
}
}

function applyObstaclePenalty(obstacle) {
bombSound.currentTime = 0;
bombSound.play();

if (obstacle.name === 'Bomb') {
score = Math.max(0, score - obstacle.penalty.points);
timeLeft = Math.max(0, timeLeft - obstacle.penalty.time);
scoreEl.textContent = `Score: ${score}`;
timerEl.textContent = `${timeLeft}s`;
showNotification('💣 Bomb Hit! -50 points, -5 seconds');
}
}

function showNotification(message) {
// Create temporary notification
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #1f2937;
color: #f9fafb;
padding: 10px 20px;
border-radius: 8px;
border: 2px solid #374151;
z-index: 1000;
font-weight: bold;
animation: slideIn 0.3s ease-out;
`;

document.body.appendChild(notification);

setTimeout(() => {
notification.remove();
}, 2000);
}

function startGame() {
clearInterval(timer);
clearInterval(moleTimer);
clearInterval(powerUpSpawnTimer);
clearInterval(obstacleSpawnTimer);

score = 0;
hole = -1;
timeLeft = 30;
lives = 3;
powerUpActive = null;
powerUpEndTime = 0;
frozenTime = 0;
powerUpHole = -1;
obstacleHole = -1;
currentPowerUp = null;
currentObstacle = null;

scoreEl.textContent = 'Score: 0';
timerEl.textContent = `${timeLeft}s`;
livesEl.textContent = `Lives: ${lives}`;
render();

const difficulty = difficultyEl.value;
const moleSpeed = speeds[difficulty];

moleTimer = setInterval(randomHole, moleSpeed);

// Spawn power-ups and obstacles at random intervals
powerUpSpawnTimer = setInterval(spawnPowerUp, 2000 + Math.random() * 3000); // Every 2-5 seconds
obstacleSpawnTimer = setInterval(spawnObstacle, 3000 + Math.random() * 4000); // Every 3-7 seconds

timer = setInterval(() => {
timeLeft--;
timerEl.textContent = `${timeLeft}s`;
if (timeLeft <= 0) endGame();
if (frozenTime <= 0) {
timeLeft--;
timerEl.textContent = `${timeLeft}s`;
if (timeLeft <= 0) endGame();
} else {
frozenTime -= 1000; // Reduce frozen time
}
}, 1000);
}

function endGame() {
clearInterval(timer);
clearInterval(moleTimer);
clearInterval(powerUpSpawnTimer);
clearInterval(obstacleSpawnTimer);

hole = -1;
powerUpHole = -1;
obstacleHole = -1;
currentPowerUp = null;
currentObstacle = null;
powerUpActive = null;
frozenTime = 0;
render();
alert(`⏱ Times up!\nYour score: ${score}`);
alert(`⏱ Time's up!\nYour score: ${score}`);
}

startBtn.addEventListener('click', startGame);
Expand Down
46 changes: 46 additions & 0 deletions projects/whack-a-mole/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ main {
background: #6ee7b7;
}

.hole.power-up {
background: #fbbf24;
animation: pulse 0.5s infinite alternate;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
}

.hole.obstacle {
background: #dc2626;
animation: shake 0.3s infinite;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
}

.hole:focus {
border-color: #6ee7b7;
}
Expand All @@ -54,3 +72,31 @@ main {
font-size: 0.9rem;
margin-top: 1rem;
}

@keyframes pulse {
from {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7);
}
to {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(251, 191, 36, 0);
}
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}