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;
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};
}
function createCardEl(card){
const el = document.createElement('div');
el.className = 'bjcard';
el.innerHTML = `
${card.rank}
${card.suit}
${card.rank}
`;
return el;
}
function createBackCardEl(){
const el = document.createElement('div');
el.className = '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 (dealerCards.length == 2 && gamePhase != 'DEALER_TURN') {
document.getElementById('dealerTotalUI').innerText = calc([dealerCards[0]]) + (dealerHiddenEls.length ? " + ??" : "")
} else {
document.getElementById('dealerTotalUI').innerText = calc(dealerCards)
}
}
function updateButtonState(){
if(gamePhase !== 'PLAYING'){
hitBtn.disabled = true;
standBtn.disabled = true;
return;
}
hitBtn.disabled = animLock;
standBtn.disabled = animLock;
}
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;
});
}
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';
}
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();
const hiddenCount = dealerHiddenEls.length;
flipAllDealerHidden();
setTimeout(()=> dealerPlay(), 300 + hiddenCount*250);
}
function startGame(){
gamePhase = 'DEALING';
updateButtonState();
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();
const p = calc(playerCards);
const playerBJ = (p === 21 && playerCards.length === 2);
if(playerBJ) stand();
}, 1000);
}
hitBtn.addEventListener('click', hit);
standBtn.addEventListener('click', stand);
window.addEventListener('resize', ()=>{
layoutOverlap(playerEls, cardsWrapper);
layoutOverlap(dealerEls, dealerWrapper);
});
startGame();