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'); const playAgainBtn = document.getElementById('playAgainBtn'); let playerCards = []; let dealerCards = []; let playerEls = []; let dealerEls = []; let dealerHiddenCards = []; let dealerHiddenEls = []; let gamePhase = 'PLAYING'; // langsung start /* ---------- 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, isDealer = false){ const wrapper = document.createElement('div'); wrapper.className = 'card-wrapper'; if (!isDealer) wrapper.style.perspective = '1000px'; wrapper.appendChild(cardEl); return wrapper; } function calc(cards){ let total=0, ace=0; for(const c of cards){ if(c.rank==='A'){ total+=11; ace++; } else if(['J','Q','K'].includes(c.rank)) total+=10; else total+=Number(c.rank); } while(total>21 && ace>0){ total-=10; ace--; } return total; } /* ---------- UI updates ---------- */ function updateTotals(){ document.getElementById('playerTotalUI').innerText = calc(playerCards); const hiddenExists = dealerHiddenEls.length > 0; if(hiddenExists) document.getElementById('dealerTotalUI').innerText = '??'; else document.getElementById('dealerTotalUI').innerText = calc(dealerCards); } /* layout overlap */ function layoutOverlap(list, wrapper){ const isDealer = (wrapper === dealerWrapper); const cardW = 110; const cardH = 154; const overlap = 28; const count = list.length; const totalWidth = cardW + Math.max(0, count-1)*overlap; const startX = (wrapper.clientWidth - totalWidth)/2; list.forEach((el, i)=>{ const x = startX + i*overlap; let y = (wrapper.clientHeight - cardH) / 2; el.style.transform = `translate3d(${x}px, ${y}px, 0)`; el.style.zIndex = 100 + i; }); } /* animation from deck */ function animateFromDeck(cardWrapper, isInitial = true){ const startX = deckEl.offsetLeft; const startY = deckEl.offsetTop; const targetTransformMatch = cardWrapper.style.transform.match(/translate3d\((.*?)px,\s*(.*?)px/); const targetX = parseFloat(targetTransformMatch ? targetTransformMatch[1] : startX); const targetY = parseFloat(targetTransformMatch ? targetTransformMatch[2] : startY); cardWrapper.style.transition = 'none'; cardWrapper.style.transform = `translate3d(${startX}px, ${startY}px, 0) scale(0.4)`; const inner = cardWrapper.querySelector('.card'); if(inner) inner.classList.add('is-moving'); requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ cardWrapper.style.transition = ''; cardWrapper.style.transform = `translate3d(${targetX}px, ${targetY}px, 0) scale(1)`; const rotZ = (Math.random() - 0.5) * 12; const rotX = isInitial ? 0 : (Math.random() - 0.5) * 10; if(inner){ inner.style.transition = 'transform .4s cubic-bezier(.68,-0.55,.27,1.55), filter .3s ease'; inner.style.transform = `rotateZ(${rotZ}deg) rotateX(${rotX}deg)`; } setTimeout(()=>{ if(inner){ inner.classList.remove('is-moving'); inner.style.transform = `rotateZ(0deg) rotateX(0deg)`; inner.style.transition = ''; } },420); }); }); } /* ---------- dealing ---------- */ function dealPlayer(){ const card = randomCard(); playerCards.push(card); const cardEl = createCardEl(card); const wrapper = wrapCardInContainer(cardEl, false); cardsWrapper.appendChild(wrapper); playerEls.push(wrapper); layoutOverlap(playerEls, cardsWrapper); animateFromDeck(wrapper); updateTotals(); } function dealDealer(faceDown=false){ const card = randomCard(); dealerCards.push(card); let wrapper; if(faceDown){ const backEl = createBackCardEl(); wrapper = wrapCardInContainer(backEl, true); dealerHiddenCards.push(card); dealerHiddenEls.push(wrapper); } else { const cardEl = createCardEl(card); wrapper = wrapCardInContainer(cardEl, true); } dealerEls.push(wrapper); dealerWrapper.appendChild(wrapper); layoutOverlap(dealerEls, dealerWrapper); animateFromDeck(wrapper); updateTotals(); } function flipAllDealerHidden(){ const hiddenWrappers = Array.from(dealerHiddenEls); hiddenWrappers.forEach((wrapper, i) => { const backEl = wrapper.querySelector('.back-card'); const cardObj = dealerHiddenCards[i]; const realEl = createCardEl(cardObj); setTimeout(()=>{ if(backEl){ backEl.style.transform = 'rotateY(90deg)'; backEl.style.opacity = '0'; } realEl.style.transform = 'rotateY(-90deg)'; realEl.style.opacity = '0'; wrapper.appendChild(realEl); setTimeout(()=>{ if(backEl) backEl.remove(); realEl.style.transform = 'rotateY(0deg)'; realEl.style.opacity = '1'; updateTotals(); },220); },300 + i*300); }); setTimeout(()=>{ dealerHiddenEls = []; dealerHiddenCards = []; }, 300 + hiddenWrappers.length*300 + 100); } /* ---------- dealer play & game end ---------- */ function dealerPlay(){ gamePhase = 'DEALER_TURN'; const cycle = setInterval(()=>{ if(calc(dealerCards) < 17){ dealDealer(false); } else { clearInterval(cycle); finishResult(); } }, 900); } function finishResult(){ gamePhase = 'END'; const p = calc(playerCards); const d = calc(dealerCards); let msg = ''; if(p > 21) msg = 'PLAYER BUST — YOU LOSE'; else if(d > 21) msg = 'DEALER BUST — YOU WIN!'; else if(p === 21 && playerCards.length === 2 && d !== 21) msg = 'BLACKJACK! — YOU WIN! (3:2)'; else if(p > d) msg = 'YOU WIN!'; else if(p < d) msg = 'YOU LOSE!'; else msg = 'PUSH (DRAW) — TARUHAN KEMBALI'; showEnd(msg); } /* ---------- end-screen handling ---------- */ function showEnd(msg){ document.getElementById('status').innerText = msg; hitBtn.disabled = true; standBtn.disabled = true; endMessage.innerText = msg; endScreen.style.display = 'flex'; playAgainBtn.focus(); } /* ---------- controls ---------- */ function hit(){ if(gamePhase !== 'PLAYING' || hitBtn.disabled) return; dealPlayer(); if(calc(playerCards) > 21){ setTimeout(() => finishResult(), 500); } } function stand(){ if(gamePhase !== 'PLAYING' || standBtn.disabled) return; gamePhase = 'DEALER_TURN'; hitBtn.disabled = true; standBtn.disabled = true; document.getElementById('status').innerText = 'DEALER TURN'; flipAllDealerHidden(); setTimeout(()=> dealerPlay(), 300 + dealerHiddenEls.length*300); } /* ---------- start / restart ---------- */ function startGame(){ // disable during dealing to prevent accidental clicks hitBtn.disabled = true; standBtn.disabled = true; playerCards = []; dealerCards = []; playerEls.forEach(e=>e.remove()); dealerEls.forEach(e=>e.remove()); playerEls = []; dealerEls = []; dealerHiddenCards = []; dealerHiddenEls = []; endScreen.style.display = 'none'; // dealing sequence setTimeout(()=> dealPlayer(), 80); setTimeout(()=> dealDealer(false), 280); // dealer first open setTimeout(()=> dealPlayer(), 480); setTimeout(()=> dealDealer(true), 680); // dealer second hidden // after dealing finished, enable buttons and set phase to PLAYING setTimeout(()=>{ gamePhase = 'PLAYING'; document.getElementById('status').innerText = 'YOUR TURN'; hitBtn.disabled = false; standBtn.disabled = false; // auto-stand on natural if(calc(playerCards) === 21) stand(); }, 900); } function restart(){ // reset and start again startGame(); } /* ---------- events ---------- */ hitBtn.addEventListener('click', hit); standBtn.addEventListener('click', stand); playAgainBtn.addEventListener('click', restart); window.addEventListener('resize', ()=>{ layoutOverlap(playerEls, cardsWrapper); layoutOverlap(dealerEls, dealerWrapper); }); /* start immediately */ startGame();