From 118194ed5960f086c3cd85cc29d432f2ea3026e3 Mon Sep 17 00:00:00 2001 From: sneha31-degug Date: Wed, 29 Oct 2025 20:32:57 +0530 Subject: [PATCH] Add Blackjack game - Closes #191 --- data/projects.json | 8 + projects/blackjack/index.html | 75 ++++++ projects/blackjack/main.js | 444 ++++++++++++++++++++++++++++++ projects/blackjack/style.css | 491 ++++++++++++++++++++++++++++++++++ 4 files changed, 1018 insertions(+) create mode 100644 projects/blackjack/index.html create mode 100644 projects/blackjack/main.js create mode 100644 projects/blackjack/style.css diff --git a/data/projects.json b/data/projects.json index 86bae25..4ea6d78 100644 --- a/data/projects.json +++ b/data/projects.json @@ -318,5 +318,13 @@ "category": "Small Games", "categoryKey": "games", "difficulty": "medium" + }, + { + "title": "Blackjack", + "slug": "blackjack", + "description": "Classic card game - Beat the dealer to 21! Features betting, realistic animations, and dark/light themes.", + "category": "Game", + "categoryKey": "game", + "difficulty": "Intermediate" } ] diff --git a/projects/blackjack/index.html b/projects/blackjack/index.html new file mode 100644 index 0000000..a69220c --- /dev/null +++ b/projects/blackjack/index.html @@ -0,0 +1,75 @@ + + + + + + Blackjack - 21 + + + +
+ +
+ +
+
+

Blackjack

+
+
+ Chips: + 1000 +
+
+ Wins: + 0 +
+
+ Losses: + 0 +
+
+
+ +
+
+
+

Dealer

+
0
+
+
+
+
+
+
+

You

+
0
+
+
+
+
+

Place Your Bet

+
+ + + + +
+
+ + +
+
Current Bet: $0
+
+ +
+
+ + + + \ No newline at end of file diff --git a/projects/blackjack/main.js b/projects/blackjack/main.js new file mode 100644 index 0000000..b59c048 --- /dev/null +++ b/projects/blackjack/main.js @@ -0,0 +1,444 @@ +// Game State +const gameState = { + deck: [], + playerHand: [], + dealerHand: [], + chips: 1000, + currentBet: 0, + wins: 0, + losses: 0, + gameActive: false, + dealerRevealed: false +}; + +// Card suits and ranks +const suits = ['♠', '♥', '♦', '♣']; +const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; +const values = { + 'A': 11, '2': 2, '3': 3, '4': 4, '5': 5, + '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, + 'J': 10, 'Q': 10, 'K': 10 +}; + +// DOM Elements +const dealerCardsEl = document.getElementById('dealerCards'); +const playerCardsEl = document.getElementById('playerCards'); +const dealerValueEl = document.getElementById('dealerValue'); +const playerValueEl = document.getElementById('playerValue'); +const gameMessageEl = document.getElementById('gameMessage'); +const chipsEl = document.getElementById('chips'); +const winsEl = document.getElementById('wins'); +const lossesEl = document.getElementById('losses'); +const currentBetEl = document.getElementById('currentBet'); +const bettingSectionEl = document.getElementById('bettingSection'); +const gameControlsEl = document.getElementById('gameControls'); +const themeBtn = document.getElementById('themeBtn'); + +// Buttons +const hitBtn = document.getElementById('hitBtn'); +const standBtn = document.getElementById('standBtn'); +const newGameBtn = document.getElementById('newGameBtn'); +const placeBetBtn = document.getElementById('placeBetBtn'); +const customBetInput = document.getElementById('customBet'); +const betButtons = document.querySelectorAll('.bet-btn'); + +// Sound effects (basic beep sounds using Web Audio API) +const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + +function playSound(frequency, duration) { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = frequency; + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + duration); +} + +function cardSound() { + playSound(400, 0.1); +} + +function winSound() { + playSound(800, 0.2); + setTimeout(() => playSound(1000, 0.2), 100); +} + +function loseSound() { + playSound(200, 0.3); +} + +// Theme Toggle +function initTheme() { + const savedTheme = localStorage.getItem('blackjack-theme') || 'dark'; + document.documentElement.setAttribute('data-theme', savedTheme); + updateThemeIcon(savedTheme); +} + +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('blackjack-theme', newTheme); + updateThemeIcon(newTheme); +} + +function updateThemeIcon(theme) { + const icon = themeBtn.querySelector('.icon'); + icon.textContent = theme === 'dark' ? '🌙' : '☀️'; +} + +// Initialize Deck +function createDeck() { + const deck = []; + for (let suit of suits) { + for (let rank of ranks) { + deck.push({ rank, suit }); + } + } + return shuffleDeck(deck); +} + +function shuffleDeck(deck) { + for (let i = deck.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [deck[i], deck[j]] = [deck[j], deck[i]]; + } + return deck; +} + +// Card Value Calculation +function calculateHandValue(hand) { + let value = 0; + let aces = 0; + + for (let card of hand) { + value += values[card.rank]; + if (card.rank === 'A') aces++; + } + + // Adjust for Aces + while (value > 21 && aces > 0) { + value -= 10; + aces--; + } + + return value; +} + +// Display Functions +function createCardElement(card, isHidden = false) { + const cardEl = document.createElement('div'); + cardEl.className = 'card'; + + if (isHidden) { + cardEl.classList.add('back'); + return cardEl; + } + + const color = (card.suit === '♥' || card.suit === '♦') ? 'red' : 'black'; + cardEl.classList.add(color); + + cardEl.innerHTML = ` +
${card.rank}
+
${card.suit}
+
${card.rank}
+ `; + + return cardEl; +} + +function displayHands(hideDealer = true) { + // Display dealer hand + dealerCardsEl.innerHTML = ''; + gameState.dealerHand.forEach((card, index) => { + const isHidden = hideDealer && index === 1 && !gameState.dealerRevealed; + dealerCardsEl.appendChild(createCardElement(card, isHidden)); + }); + + // Display player hand + playerCardsEl.innerHTML = ''; + gameState.playerHand.forEach(card => { + playerCardsEl.appendChild(createCardElement(card)); + }); + + // Update values + const dealerValue = calculateHandValue(gameState.dealerHand); + const playerValue = calculateHandValue(gameState.playerHand); + + if (hideDealer && !gameState.dealerRevealed) { + const firstCardValue = values[gameState.dealerHand[0].rank]; + dealerValueEl.textContent = firstCardValue; + } else { + dealerValueEl.textContent = dealerValue; + } + + playerValueEl.textContent = playerValue; +} + +function updateStats() { + chipsEl.textContent = gameState.chips; + winsEl.textContent = gameState.wins; + lossesEl.textContent = gameState.losses; + currentBetEl.textContent = gameState.currentBet; +} + +function showMessage(message, type = '') { + gameMessageEl.textContent = message; + gameMessageEl.className = 'game-message'; + if (type) { + gameMessageEl.classList.add(type); + } +} + +// Betting Functions +function placeBet(amount) { + if (amount > gameState.chips) { + showMessage('Not enough chips!', 'lose'); + return false; + } + + if (amount <= 0) { + showMessage('Invalid bet amount!', 'lose'); + return false; + } + + gameState.currentBet = amount; + gameState.chips -= amount; + updateStats(); + startGame(); + return true; +} + +// Game Flow +function startGame() { + gameState.deck = createDeck(); + gameState.playerHand = []; + gameState.dealerHand = []; + gameState.gameActive = true; + gameState.dealerRevealed = false; + + showMessage(''); + bettingSectionEl.style.display = 'none'; + gameControlsEl.style.display = 'flex'; + + // Deal initial cards + setTimeout(() => { + dealCard(gameState.playerHand); + cardSound(); + }, 200); + + setTimeout(() => { + dealCard(gameState.dealerHand); + cardSound(); + }, 400); + + setTimeout(() => { + dealCard(gameState.playerHand); + cardSound(); + }, 600); + + setTimeout(() => { + dealCard(gameState.dealerHand); + cardSound(); + displayHands(true); + checkForBlackjack(); + }, 800); +} + +function dealCard(hand) { + const card = gameState.deck.pop(); + hand.push(card); + displayHands(!gameState.dealerRevealed); +} + +function checkForBlackjack() { + const playerValue = calculateHandValue(gameState.playerHand); + const dealerValue = calculateHandValue(gameState.dealerHand); + + if (playerValue === 21) { + if (dealerValue === 21) { + endGame('draw', 'Both Blackjack! Push!'); + } else { + endGame('win', 'Blackjack! You Win!'); + } + } +} + +function hit() { + if (!gameState.gameActive) return; + + dealCard(gameState.playerHand); + cardSound(); + + const playerValue = calculateHandValue(gameState.playerHand); + + if (playerValue > 21) { + endGame('lose', 'Bust! You Lose!'); + } else if (playerValue === 21) { + stand(); + } +} + +function stand() { + if (!gameState.gameActive) return; + + gameState.dealerRevealed = true; + hitBtn.disabled = true; + standBtn.disabled = true; + + dealerPlay(); +} + +function dealerPlay() { + displayHands(false); + + const dealerValue = calculateHandValue(gameState.dealerHand); + + if (dealerValue < 17) { + setTimeout(() => { + dealCard(gameState.dealerHand); + cardSound(); + dealerPlay(); + }, 1000); + } else { + determineWinner(); + } +} + +function determineWinner() { + const playerValue = calculateHandValue(gameState.playerHand); + const dealerValue = calculateHandValue(gameState.dealerHand); + + if (dealerValue > 21) { + endGame('win', 'Dealer Busts! You Win!'); + } else if (playerValue > dealerValue) { + endGame('win', 'You Win!'); + } else if (dealerValue > playerValue) { + endGame('lose', 'Dealer Wins!'); + } else { + endGame('draw', 'Push! It\'s a Draw!'); + } +} + +function endGame(result, message) { + gameState.gameActive = false; + hitBtn.disabled = true; + standBtn.disabled = true; + + showMessage(message, result); + + if (result === 'win') { + const isBlackjack = calculateHandValue(gameState.playerHand) === 21 && + gameState.playerHand.length === 2; + const winnings = isBlackjack ? + Math.floor(gameState.currentBet * 2.5) : + gameState.currentBet * 2; + gameState.chips += winnings; + gameState.wins++; + winSound(); + } else if (result === 'lose') { + gameState.losses++; + loseSound(); + } else { + gameState.chips += gameState.currentBet; + cardSound(); + } + + updateStats(); + + if (gameState.chips <= 0) { + setTimeout(() => { + alert('Game Over! You\'re out of chips. Resetting...'); + resetGame(); + }, 2000); + } +} + +function newGame() { + if (gameState.chips <= 0) { + resetGame(); + return; + } + + gameState.currentBet = 0; + gameState.playerHand = []; + gameState.dealerHand = []; + gameState.gameActive = false; + gameState.dealerRevealed = false; + + dealerCardsEl.innerHTML = ''; + playerCardsEl.innerHTML = ''; + dealerValueEl.textContent = '0'; + playerValueEl.textContent = '0'; + showMessage(''); + + bettingSectionEl.style.display = 'block'; + gameControlsEl.style.display = 'none'; + customBetInput.value = ''; + + hitBtn.disabled = false; + standBtn.disabled = false; + + updateStats(); +} + +function resetGame() { + gameState.chips = 1000; + gameState.wins = 0; + gameState.losses = 0; + newGame(); +} + +// Event Listeners +hitBtn.addEventListener('click', hit); +standBtn.addEventListener('click', stand); +newGameBtn.addEventListener('click', newGame); +themeBtn.addEventListener('click', toggleTheme); + +betButtons.forEach(btn => { + btn.addEventListener('click', () => { + const betAmount = parseInt(btn.dataset.bet); + placeBet(betAmount); + }); +}); + +placeBetBtn.addEventListener('click', () => { + const betAmount = parseInt(customBetInput.value); + if (betAmount) { + placeBet(betAmount); + } else { + showMessage('Enter a valid bet amount!', 'lose'); + } +}); + +customBetInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + const betAmount = parseInt(customBetInput.value); + if (betAmount) { + placeBet(betAmount); + } + } +}); + +// Keyboard Controls +document.addEventListener('keydown', (e) => { + if (!gameState.gameActive) return; + + if (e.key === 'h' || e.key === 'H') { + hit(); + } else if (e.key === 's' || e.key === 'S') { + stand(); + } +}); + +// Initialize +initTheme(); +updateStats(); \ No newline at end of file diff --git a/projects/blackjack/style.css b/projects/blackjack/style.css new file mode 100644 index 0000000..d4f6759 --- /dev/null +++ b/projects/blackjack/style.css @@ -0,0 +1,491 @@ +:root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-table: #1a472a; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --accent: #10b981; + --accent-hover: #059669; + --card-bg: #ffffff; + --card-shadow: rgba(0, 0, 0, 0.3); + --border: #334155; + --message-win: #10b981; + --message-lose: #ef4444; + --message-draw: #f59e0b; +} + +[data-theme="light"] { + --bg-primary: #f1f5f9; + --bg-secondary: #e2e8f0; + --bg-table: #2d6a3e; + --text-primary: #0f172a; + --text-secondary: #475569; + --accent: #059669; + --accent-hover: #047857; + --card-bg: #ffffff; + --card-shadow: rgba(0, 0, 0, 0.2); + --border: #cbd5e1; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; +} + +#themeBtn { + background: var(--bg-secondary); + border: 2px solid var(--border); + border-radius: 50%; + width: 50px; + height: 50px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + transition: all 0.3s ease; +} + +#themeBtn:hover { + transform: scale(1.1); + border-color: var(--accent); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; +} + +h1 { + font-size: 3rem; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.stats { + display: flex; + justify-content: center; + gap: 30px; + flex-wrap: wrap; +} + +.stat-item { + background: var(--bg-secondary); + padding: 15px 25px; + border-radius: 10px; + border: 2px solid var(--border); +} + +.stat-label { + color: var(--text-secondary); + font-size: 0.9rem; + margin-right: 8px; +} + +.stat-value { + font-size: 1.3rem; + font-weight: bold; + color: var(--accent); +} + +.game-area { + background: var(--bg-table); + border-radius: 20px; + padding: 40px 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); + min-height: 600px; + position: relative; +} + +.dealer-section, +.player-section { + margin-bottom: 40px; +} + +.player-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 0 20px; +} + +.player-info h2 { + font-size: 1.5rem; +} + +.hand-value { + background: var(--bg-secondary); + padding: 8px 20px; + border-radius: 8px; + font-size: 1.2rem; + font-weight: bold; + border: 2px solid var(--border); +} + +.cards-container { + display: flex; + justify-content: center; + gap: 15px; + flex-wrap: wrap; + min-height: 140px; + align-items: center; +} + +.card { + width: 90px; + height: 130px; + background: var(--card-bg); + border-radius: 8px; + box-shadow: 0 4px 8px var(--card-shadow); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px; + position: relative; + animation: dealCard 0.3s ease-out; + transition: transform 0.2s ease; +} + +.card:hover { + transform: translateY(-5px); +} + +@keyframes dealCard { + from { + opacity: 0; + transform: translateY(-50px) rotateY(180deg); + } + to { + opacity: 1; + transform: translateY(0) rotateY(0); + } +} + +.card.back { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + justify-content: center; + align-items: center; +} + +.card.back::before { + content: "🂠"; + font-size: 4rem; + color: rgba(255, 255, 255, 0.3); +} + +.card-rank { + font-size: 1.5rem; + font-weight: bold; +} + +.card-suit { + font-size: 2rem; + text-align: center; +} + +.card-rank-bottom { + font-size: 1.5rem; + font-weight: bold; + text-align: right; +} + +.card.red { + color: #dc2626; +} + +.card.black { + color: #0f172a; +} + +.game-message { + text-align: center; + font-size: 2rem; + font-weight: bold; + margin: 30px 0; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; + animation: messageAppear 0.5s ease-out; +} + +@keyframes messageAppear { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.game-message.win { + color: var(--message-win); + text-shadow: 0 0 20px var(--message-win); +} + +.game-message.lose { + color: var(--message-lose); + text-shadow: 0 0 20px var(--message-lose); +} + +.game-message.draw { + color: var(--message-draw); + text-shadow: 0 0 20px var(--message-draw); +} + +.betting-section { + text-align: center; + padding: 30px; + background: var(--bg-secondary); + border-radius: 15px; + margin: 20px auto; + max-width: 600px; +} + +.betting-section h3 { + margin-bottom: 20px; + font-size: 1.5rem; +} + +.bet-controls { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.bet-btn { + background: var(--accent); + color: white; + border: none; + padding: 15px 30px; + border-radius: 8px; + font-size: 1.1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +.bet-btn:hover { + background: var(--accent-hover); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(16, 185, 129, 0.3); +} + +.custom-bet { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +#customBet { + padding: 12px 20px; + border-radius: 8px; + border: 2px solid var(--border); + background: var(--bg-primary); + color: var(--text-primary); + font-size: 1rem; + width: 200px; +} + +#placeBetBtn { + background: var(--accent); + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +#placeBetBtn:hover { + background: var(--accent-hover); +} + +.current-bet { + font-size: 1.3rem; + font-weight: bold; + color: var(--accent); +} + +.controls { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 30px; + flex-wrap: wrap; +} + +.control-btn { + padding: 15px 40px; + font-size: 1.2rem; + font-weight: bold; + border: none; + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 1px; +} + +.control-btn.primary { + background: var(--accent); + color: white; +} + +.control-btn.primary:hover { + background: var(--accent-hover); + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(16, 185, 129, 0.3); +} + +.control-btn.secondary { + background: #f59e0b; + color: white; +} + +.control-btn.secondary:hover { + background: #d97706; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(245, 158, 11, 0.3); +} + +.control-btn.tertiary { + background: #6366f1; + color: white; +} + +.control-btn.tertiary:hover { + background: #4f46e5; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(99, 102, 241, 0.3); +} + +.control-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +@media (max-width: 768px) { + h1 { + font-size: 2rem; + } + + .stats { + gap: 15px; + } + + .stat-item { + padding: 10px 15px; + } + + .game-area { + padding: 20px 10px; + } + + .card { + width: 70px; + height: 100px; + padding: 8px; + } + + .card-rank, + .card-rank-bottom { + font-size: 1.2rem; + } + + .card-suit { + font-size: 1.5rem; + } + + .game-message { + font-size: 1.5rem; + } + + .control-btn { + padding: 12px 25px; + font-size: 1rem; + } + + .betting-section { + padding: 20px; + } + + .bet-controls { + gap: 10px; + } + + .bet-btn { + padding: 12px 20px; + font-size: 1rem; + } +} + +@media (max-width: 480px) { + .container { + padding: 10px; + } + + h1 { + font-size: 1.5rem; + } + + .cards-container { + gap: 8px; + } + + .card { + width: 60px; + height: 85px; + padding: 5px; + } + + .card-rank, + .card-rank-bottom { + font-size: 1rem; + } + + .card-suit { + font-size: 1.2rem; + } + + .controls { + gap: 10px; + } + + .control-btn { + padding: 10px 20px; + font-size: 0.9rem; + } +} \ No newline at end of file