footer
This commit is contained in:
parent
30d62eb967
commit
e0623117d7
275
public/assets/js/game.js
Normal file
275
public/assets/js/game.js
Normal file
@ -0,0 +1,275 @@
|
||||
|
||||
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();
|
||||
247
public/game.php
Normal file
247
public/game.php
Normal file
@ -0,0 +1,247 @@
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Hit or Run</title>
|
||||
<style>
|
||||
:root{
|
||||
--card-w:110px;
|
||||
--card-h:154px;
|
||||
--deck-x: 800px;
|
||||
--deck-y: 100px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0;
|
||||
min-height:100vh;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-family:Inter,system-ui,Arial;
|
||||
background:#071827
|
||||
}
|
||||
.stage{
|
||||
width:960px;
|
||||
height:540px;
|
||||
border-radius:12px;
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
box-shadow:0 18px 50px rgba(0,0,0,.6)
|
||||
}
|
||||
.bg{
|
||||
position:absolute;
|
||||
inset:0; background:#153f28
|
||||
}
|
||||
.overlay{
|
||||
position:absolute; inset:0;
|
||||
background:linear-gradient(180deg, rgba(11,31,20,.45), rgba(8,24,16,.65));
|
||||
display:flex; flex-direction:column; padding:18px
|
||||
}
|
||||
.header{
|
||||
color:#dff6e9;
|
||||
font-weight:700;
|
||||
display:flex;
|
||||
justify-content:space-between
|
||||
}
|
||||
.table-area{
|
||||
flex:1; display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
position:relative
|
||||
}
|
||||
.deck {
|
||||
position: absolute;
|
||||
left: var(--deck-x);
|
||||
top: var(--deck-y);
|
||||
width: 82px;
|
||||
height: 120px;
|
||||
border-radius: 10px;
|
||||
background: #113625;
|
||||
box-shadow: 0 0 0 2px #0d291c inset, 0 10px 20px rgba(0,0,0,0.55);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
color:#b9e4d0;
|
||||
font-weight:900;
|
||||
font-size:14px;
|
||||
letter-spacing:2px;
|
||||
z-index:500;
|
||||
}
|
||||
.player-zone{
|
||||
position:absolute;
|
||||
left:37%;
|
||||
bottom:18px;
|
||||
transform:translateX(-50%);
|
||||
width:78%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center
|
||||
}
|
||||
.cards-wrapper{
|
||||
position:relative;
|
||||
width:580px;
|
||||
height:170px;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
}
|
||||
.dealer-zone{
|
||||
position:absolute;
|
||||
left:50%;
|
||||
top:18px;
|
||||
transform:translateX(-50%);
|
||||
width:78%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
color:#dff6e9;
|
||||
font-weight:700
|
||||
}
|
||||
.dealer-cards{
|
||||
position:relative;
|
||||
width:580px;
|
||||
height:170px
|
||||
}
|
||||
.card-wrapper {
|
||||
position: absolute;
|
||||
width: var(--card-w);
|
||||
height: var(--card-h);
|
||||
transition: transform .4s cubic-bezier(.68,-0.55,.27,1.55);
|
||||
align-items: center;
|
||||
transform-origin: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.card{
|
||||
width:var(--card-w);
|
||||
height:var(--card-h);
|
||||
border-radius:10px;
|
||||
background:#fff;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:space-between;
|
||||
padding:10px;
|
||||
font-weight:800;
|
||||
box-shadow:0 10px 22px rgba(0,0,0,.45);
|
||||
position:absolute;
|
||||
transition: transform .4s cubic-bezier(.68,-0.55,.27,1.55), opacity .3s ease, filter .3s ease;
|
||||
backface-visibility:hidden;
|
||||
transform-origin:center center;
|
||||
color: #000;
|
||||
}
|
||||
.card.is-moving {
|
||||
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.4));
|
||||
}
|
||||
.card.red{
|
||||
color:#c42b2b
|
||||
}
|
||||
.card .corner{
|
||||
font-size:18px
|
||||
}
|
||||
.card .center{
|
||||
font-size:40px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
.back-card{
|
||||
background:#122f21;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-size:20px;
|
||||
color:#bddfcf;
|
||||
letter-spacing:4px;
|
||||
font-weight:900;
|
||||
border-radius:10px;
|
||||
}
|
||||
.controls{
|
||||
display:flex;
|
||||
gap:12px;
|
||||
margin-top:8px
|
||||
}
|
||||
button{
|
||||
background:#0f5436;
|
||||
color:#e6fff2;
|
||||
border:0;
|
||||
padding:10px 16px;
|
||||
border-radius:8px;
|
||||
cursor:pointer;
|
||||
font-weight:700
|
||||
}
|
||||
button:disabled{
|
||||
background:#2d2d2d;
|
||||
opacity:.5;
|
||||
cursor:not-allowed
|
||||
}
|
||||
.end-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
backdrop-filter: blur(6px);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
font-size: 48px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
z-index: 999;
|
||||
}
|
||||
.end-screen button {
|
||||
margin-top: 22px;
|
||||
font-size: 20px;
|
||||
padding: 12px 26px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #18a56d;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
.end-screen button:hover {
|
||||
background: #1ccf86;
|
||||
}
|
||||
</style>
|
||||
</head></script>
|
||||
<body>
|
||||
<div class="stage" id="stage">
|
||||
<div class="bg"></div>
|
||||
<div class="overlay">
|
||||
<div class="header">
|
||||
<div>Your Total Card: <strong id="playerTotalUI">0</strong></div>
|
||||
<div id="status">PLACE YOUR BET</div>
|
||||
</div>
|
||||
|
||||
<div id="deck" class="deck" aria-hidden="true">DECK</div>
|
||||
|
||||
<div class="table-area">
|
||||
<div class="dealer-zone" aria-live="polite">
|
||||
<div class="dealer-label">DEALER</div>
|
||||
<div class="dealer-cards" id="dealerWrapper" aria-hidden="false"></div>
|
||||
<div class="dealer-label">Total Dealer: <span id="dealerTotalUI">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="player-zone">
|
||||
<div style="display:flex;justify-content:center;align-items:center;width:100%;color:#dff6e9;font-weight:700;margin-bottom:8px;gap:40px">
|
||||
<div class="cards-wrapper" id="cardsWrapper" aria-live="polite"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="stand" disabled>STAND</button>
|
||||
<button id="hit" disabled>HIT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="end-screen" id="endScreen" role="dialog" aria-modal="true">
|
||||
<div id="endMessage">RESULT</div>
|
||||
<button id="playAgainBtn">PLAY AGAIN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -19,7 +19,7 @@
|
||||
header("Location: home");
|
||||
exit;
|
||||
} else {
|
||||
$error = "Wrong Username or Password.";
|
||||
$error = "Wrong Username or Password.";s
|
||||
}
|
||||
} else {
|
||||
$error = "Username not found.";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user