347 lines
8.7 KiB
JavaScript
347 lines
8.7 KiB
JavaScript
/* ===========================================
|
|
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 = `
|
|
<div class="corner">${card.rank}</div>
|
|
<div class="center">${card.suit}</div>
|
|
<div class="corner" style="transform:rotate(180deg)">${card.rank}</div>
|
|
`;
|
|
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();
|