/* =========================================== GLOBALS & SETUP =========================================== */ const ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']; const suits = ['♠','♥','♦','♣']; const cardsWrapper = document.getElementById('cardsWrapper'); const dealerWrapper = document.getElementById('dealerWrapper'); const deckEl = document.getElementById('deck'); const hitBtn = document.getElementById('hit'); const standBtn = document.getElementById('stand'); const endScreen = document.getElementById('endScreen'); const endMessage = document.getElementById('endMessage'); let playerCards = []; let dealerCards = []; let playerEls = []; let dealerEls = []; let dealerHiddenCards = []; let dealerHiddenEls = []; let gamePhase = 'INIT'; let animLock = false; // prevents button spam /* =========================================== HELPERS =========================================== */ function randomCard(){ const r = ranks[Math.floor(Math.random()*ranks.length)]; const s = suits[Math.floor(Math.random()*suits.length)]; return {rank:r, suit:s, color:(s==='♥'||s==='♦')?'red':'black'}; } function createCardEl(card){ const el = document.createElement('div'); el.className = 'card' + (card.color==='red' ? ' red' : ''); el.innerHTML = `
${card.rank}
${card.suit}
${card.rank}
`; return el; } function createBackCardEl(){ const el = document.createElement('div'); el.className = 'card back-card'; el.innerText = 'HIT'; return el; } function wrapCardInContainer(cardEl){ const wrapper = document.createElement('div'); wrapper.className = 'card-wrapper'; wrapper.style.perspective = '1000px'; wrapper.appendChild(cardEl); return wrapper; } function calc(cards){ let t = 0, aces = 0; for(const c of cards){ if(c.rank === 'A'){ t += 11; aces++; } else if(['J','Q','K'].includes(c.rank)) t += 10; else t += Number(c.rank); } while(t > 21 && aces > 0){ t -= 10; aces--; } return t; } function updateTotals(){ document.getElementById('playerTotalUI').innerText = calc(playerCards); if(dealerHiddenEls.length){ document.getElementById('dealerTotalUI').innerText = '??'; } else { document.getElementById('dealerTotalUI').innerText = calc(dealerCards); } } /* =========================================== BUTTON ENABLE/DISABLE HANDLING =========================================== */ function updateButtonState(){ if(gamePhase !== 'PLAYING'){ hitBtn.disabled = true; standBtn.disabled = true; return; } hitBtn.disabled = animLock; standBtn.disabled = animLock; } /* =========================================== LAYOUT =========================================== */ function layoutOverlap(list, wrapper){ const cardW = 110; const cardH = 154; const overlap = 28; const count = list.length; const totalW = cardW + Math.max(0, count - 1)*overlap; const startX = (wrapper.clientWidth - totalW) / 2; const startY = (wrapper.clientHeight - cardH) / 2; list.forEach((el, i) => { const x = startX + i*overlap; el.style.transform = `translate3d(${x}px, ${startY}px, 0)`; el.style.zIndex = 100 + i; }); } /* =========================================== ANIMATION FROM DECK =========================================== */ function animateFromDeck(wrapper){ animLock = true; updateButtonState(); const targetX = parseFloat(wrapper.style.transform.match(/translate3d\((.*?)px/)?.[1] || 0); const targetY = parseFloat(wrapper.style.transform.match(/,\s*(.*?)px/)?.[1] || 0); const startX = deckEl.offsetLeft; const startY = deckEl.offsetTop; wrapper.style.transition = 'none'; wrapper.style.transform = `translate3d(${startX}px, ${startY}px, 0) scale(0.4)`; const inner = wrapper.querySelector('.card'); requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ wrapper.style.transition = ''; wrapper.style.transform = `translate3d(${targetX}px, ${targetY}px, 0) scale(1)`; if(inner){ inner.style.transition = 'transform .4s cubic-bezier(.68,-0.55,.27,1.55)'; inner.style.transform = `rotateZ(${(Math.random()-0.5)*12}deg)`; } setTimeout(()=>{ if(inner){ inner.style.transform = ''; inner.style.transition = ''; } animLock = false; updateButtonState(); }, 420); }); }); } function dealPlayer(){ const card = randomCard(); playerCards.push(card); const el = createCardEl(card); const wrap = wrapCardInContainer(el); cardsWrapper.appendChild(wrap); playerEls.push(wrap); layoutOverlap(playerEls, cardsWrapper); animateFromDeck(wrap); updateTotals(); } function dealDealer(faceDown=false){ const card = randomCard(); dealerCards.push(card); let wrap; if(faceDown){ const back = createBackCardEl(); wrap = wrapCardInContainer(back); dealerHiddenCards.push(card); dealerHiddenEls.push(wrap); } else { const el = createCardEl(card); wrap = wrapCardInContainer(el); } dealerEls.push(wrap); dealerWrapper.appendChild(wrap); layoutOverlap(dealerEls, dealerWrapper); animateFromDeck(wrap); updateTotals(); } function flipAllDealerHidden(){ animLock = true; updateButtonState(); const hidden = Array.from(dealerHiddenEls); hidden.forEach((wrapper, i)=>{ const back = wrapper.querySelector('.back-card'); const card = dealerHiddenCards[i]; const real = createCardEl(card); setTimeout(()=>{ if(back){ back.style.transform = 'rotateY(90deg)'; back.style.opacity = '0'; } real.style.transform = 'rotateY(-90deg)'; real.style.opacity = '0'; wrapper.appendChild(real); setTimeout(()=>{ if(back) back.remove(); real.style.transform = 'rotateY(0deg)'; real.style.opacity = '1'; updateTotals(); }, 220); }, 250 + i*250); }); setTimeout(()=>{ dealerHiddenCards = []; dealerHiddenEls = []; animLock = false; updateButtonState(); }, 250 + hidden.length*250 + 300); } function dealerPlay(){ gamePhase = 'DEALER_TURN'; updateButtonState(); const interval = setInterval(()=>{ if(calc(dealerCards) < 17){ dealDealer(false); } else { clearInterval(interval); finishResult(); } }, 900); } function finishResult(){ gamePhase = 'END'; updateButtonState(); const p = calc(playerCards); const d = calc(dealerCards); const playerBJ = (p === 21 && playerCards.length === 2); const dealerBJ = (d === 21 && dealerCards.length === 2); let msg = ''; if(playerBJ && dealerBJ) msg = 'PUSH — BOTH BLACKJACK'; else if(playerBJ) msg = 'BLACKJACK! YOU WIN (3:2)'; else if(dealerBJ) msg = 'DEALER BLACKJACK — YOU LOSE'; else if(p > 21) msg = 'PLAYER BUST — YOU LOSE'; else if(d > 21) msg = 'DEALER BUST — YOU WIN!'; else if(p > d) msg = 'YOU WIN!'; else if(p < d) msg = 'YOU LOSE!'; else msg = 'PUSH (DRAW)'; endMessage.innerText = msg; endScreen.style.display = 'flex'; document.getElementById('status').innerText = msg; } function hit(){ if(gamePhase !== 'PLAYING' || animLock) return; updateButtonState(); dealPlayer(); if(calc(playerCards) > 21){ setTimeout(()=> finishResult(), 500); } } function stand(){ if(gamePhase !== 'PLAYING' || animLock) return; gamePhase = 'DEALER_TURN'; updateButtonState(); document.getElementById('status').innerText = 'DEALER TURN'; const hiddenCount = dealerHiddenEls.length; flipAllDealerHidden(); setTimeout(()=> dealerPlay(), 300 + hiddenCount*250); } function startGame(){ gamePhase = 'DEALING'; updateButtonState(); // cleanup previous if any playerCards = []; dealerCards = []; playerEls.forEach(e=>e.remove()); dealerEls.forEach(e=>e.remove()); playerEls = []; dealerEls = []; dealerHiddenCards = []; dealerHiddenEls = []; endScreen.style.display = 'none'; setTimeout(()=>dealPlayer(), 100); setTimeout(()=>dealDealer(false), 350); setTimeout(()=>dealPlayer(), 600); setTimeout(()=>dealDealer(true), 850); setTimeout(()=>{ gamePhase = 'PLAYING'; updateButtonState(); document.getElementById('status').innerText = 'YOUR TURN'; const p = calc(playerCards); const playerBJ = (p === 21 && playerCards.length === 2); if(playerBJ) stand(); // natural blackjack auto-resolve }, 1000); } /* =========================================== EVENTS =========================================== */ hitBtn.addEventListener('click', hit); standBtn.addEventListener('click', stand); window.addEventListener('resize', ()=>{ layoutOverlap(playerEls, cardsWrapper); layoutOverlap(dealerEls, dealerWrapper); }); /* START GAME ONLY ONCE */ startGame();