276 lines
8.7 KiB
JavaScript
276 lines
8.7 KiB
JavaScript
|
|
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 = `
|
|
<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, 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();
|