diff --git a/ingame.html b/ingame.html
index ad0927d..4240442 100644
--- a/ingame.html
+++ b/ingame.html
@@ -211,7 +211,6 @@
DECK
diff --git a/public/assets/css/style.css b/public/assets/css/style.css
index d3e51ef..ba0036d 100644
--- a/public/assets/css/style.css
+++ b/public/assets/css/style.css
@@ -1,21 +1,35 @@
-body, html{
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
- margin:0;
background: radial-gradient(circle at 10% 10%, rgba(255,255,255,0.03), transparent 5%),
linear-gradient(180deg, #000 0%, #071827 100%),
var(--bg);
- color:var(--muted);
- -webkit-font-smoothing:antialiased;
- -moz-osx-font-smoothing:grayscale;
- padding:24px;
- display:flex;
- align-items:center;
- justify-content:center;
- height:100%;
- overflow: hidden;
+ color: var(--muted);
+}
+
+.main-wrapper {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px;
+ width: 100%;
}
:root{
+ --card-w:110px;
+ --card-h:154px;
+ --deck-x: 800px;
+ --deck-y: 100px;
--bg:#071827;
--card:#071827;
--accent:#ffd54a;
@@ -253,8 +267,8 @@ footer{
margin-top: 0 !important;
}
-.mb16 {
- margin-bottom: 16px;
+.mb24 {
+ margin-bottom: 24px;
}
/* Chrome, Edge, Safari */
@@ -268,4 +282,195 @@ input::-webkit-inner-spin-button {
input[type="number"] {
-moz-appearance: textfield; /* old syntax */
appearance: textfield; /* standard property */
-}
\ No newline at end of file
+}
+
+footer {
+ margin-top: auto; /* pushes footer to bottom */
+ color: #fff;
+ padding: 1rem;
+}
+
+table#leaderboard thead tr th.rank, table#leaderboard tbody tr td.rank{
+ width: 20%;
+ font-size: 16px;
+}
+
+table#leaderboard thead tr th, table#leaderboard tbody tr td{
+ width: 40%;
+ font-size: 20px;
+}
+
+
+ :root{
+
+ }
+
+ .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;
+ display: hidden;
+ 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;
+ }
diff --git a/public/assets/js/game.js b/public/assets/js/game.js
index 6826907..dd6e7fb 100644
--- a/public/assets/js/game.js
+++ b/public/assets/js/game.js
@@ -1,4 +1,6 @@
-
+/* ===========================================
+ GLOBALS & SETUP
+ =========================================== */
const ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
const suits = ['♠','♥','♦','♣'];
@@ -10,7 +12,6 @@ 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 = [];
@@ -18,9 +19,14 @@ let playerEls = [];
let dealerEls = [];
let dealerHiddenCards = [];
let dealerHiddenEls = [];
-let gamePhase = 'PLAYING'; // langsung start
-/* ---------- helpers ---------- */
+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)];
@@ -45,147 +51,198 @@ function createBackCardEl(){
return el;
}
-function wrapCardInContainer(cardEl, isDealer = false){
+function wrapCardInContainer(cardEl){
const wrapper = document.createElement('div');
wrapper.className = 'card-wrapper';
- if (!isDealer) wrapper.style.perspective = '1000px';
+ wrapper.style.perspective = '1000px';
wrapper.appendChild(cardEl);
return wrapper;
}
function calc(cards){
- let total=0, ace=0;
+ let t = 0, aces = 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);
+ if(c.rank === 'A'){ t += 11; aces++; }
+ else if(['J','Q','K'].includes(c.rank)) t += 10;
+ else t += Number(c.rank);
}
- while(total>21 && ace>0){ total-=10; ace--; }
- return total;
+ while(t > 21 && aces > 0){
+ t -= 10; aces--;
+ }
+ return t;
}
-/* ---------- 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);
+ if(dealerHiddenEls.length){
+ document.getElementById('dealerTotalUI').innerText = '??';
+ } else {
+ document.getElementById('dealerTotalUI').innerText = calc(dealerCards);
+ }
}
-/* layout overlap */
+
+/* ===========================================
+ 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 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 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;
- let y = (wrapper.clientHeight - cardH) / 2;
- el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+ el.style.transform = `translate3d(${x}px, ${startY}px, 0)`;
el.style.zIndex = 100 + i;
});
}
-/* animation from deck */
-function animateFromDeck(cardWrapper, isInitial = true){
+
+/* ===========================================
+ 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;
- 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');
+
+ wrapper.style.transition = 'none';
+ wrapper.style.transform = `translate3d(${startX}px, ${startY}px, 0) scale(0.4)`;
+
+ const inner = wrapper.querySelector('.card');
+
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;
+ 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), filter .3s ease';
- inner.style.transform = `rotateZ(${rotZ}deg) rotateX(${rotX}deg)`;
+ 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.classList.remove('is-moving');
- inner.style.transform = `rotateZ(0deg) rotateX(0deg)`;
+ inner.style.transform = '';
inner.style.transition = '';
}
- },420);
+ animLock = false;
+ updateButtonState();
+ }, 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);
+
+ const el = createCardEl(card);
+ const wrap = wrapCardInContainer(el);
+ cardsWrapper.appendChild(wrap);
+
+ playerEls.push(wrap);
layoutOverlap(playerEls, cardsWrapper);
- animateFromDeck(wrapper);
+ animateFromDeck(wrap);
+
updateTotals();
}
function dealDealer(faceDown=false){
const card = randomCard();
dealerCards.push(card);
- let wrapper;
+
+ let wrap;
if(faceDown){
- const backEl = createBackCardEl();
- wrapper = wrapCardInContainer(backEl, true);
+ const back = createBackCardEl();
+ wrap = wrapCardInContainer(back);
dealerHiddenCards.push(card);
- dealerHiddenEls.push(wrapper);
+ dealerHiddenEls.push(wrap);
} else {
- const cardEl = createCardEl(card);
- wrapper = wrapCardInContainer(cardEl, true);
+ const el = createCardEl(card);
+ wrap = wrapCardInContainer(el);
}
- dealerEls.push(wrapper);
- dealerWrapper.appendChild(wrapper);
+
+ dealerEls.push(wrap);
+ dealerWrapper.appendChild(wrap);
+
layoutOverlap(dealerEls, dealerWrapper);
- animateFromDeck(wrapper);
+ animateFromDeck(wrap);
+
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);
+ 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(backEl){ backEl.style.transform = 'rotateY(90deg)'; backEl.style.opacity = '0'; }
- realEl.style.transform = 'rotateY(-90deg)'; realEl.style.opacity = '0';
- wrapper.appendChild(realEl);
+ 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(backEl) backEl.remove();
- realEl.style.transform = 'rotateY(0deg)';
- realEl.style.opacity = '1';
+ if(back) back.remove();
+ real.style.transform = 'rotateY(0deg)';
+ real.style.opacity = '1';
updateTotals();
- },220);
- },300 + i*300);
+ }, 220);
+
+ }, 250 + i*250);
});
+
setTimeout(()=>{
- dealerHiddenEls = [];
dealerHiddenCards = [];
- }, 300 + hiddenWrappers.length*300 + 100);
+ dealerHiddenEls = [];
+ animLock = false;
+ updateButtonState();
+ }, 250 + hidden.length*250 + 300);
}
-/* ---------- dealer play & game end ---------- */
function dealerPlay(){
gamePhase = 'DEALER_TURN';
- const cycle = setInterval(()=>{
+ updateButtonState();
+
+ const interval = setInterval(()=>{
if(calc(dealerCards) < 17){
dealDealer(false);
} else {
- clearInterval(cycle);
+ clearInterval(interval);
finishResult();
}
}, 900);
@@ -193,83 +250,97 @@ function dealerPlay(){
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(p > 21) msg = 'PLAYER BUST — YOU LOSE';
+
+ 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 === 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);
-}
+ else msg = 'PUSH (DRAW)';
-/* ---------- 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();
+ document.getElementById('status').innerText = msg;
}
-/* ---------- controls ---------- */
function hit(){
- if(gamePhase !== 'PLAYING' || hitBtn.disabled) return;
+ if(gamePhase !== 'PLAYING' || animLock) return;
+ updateButtonState();
+
dealPlayer();
if(calc(playerCards) > 21){
- setTimeout(() => finishResult(), 500);
+ setTimeout(()=> finishResult(), 500);
}
}
function stand(){
- if(gamePhase !== 'PLAYING' || standBtn.disabled) return;
+ if(gamePhase !== 'PLAYING' || animLock) return;
+
gamePhase = 'DEALER_TURN';
- hitBtn.disabled = true;
- standBtn.disabled = true;
+ updateButtonState();
+
document.getElementById('status').innerText = 'DEALER TURN';
+
+ const hiddenCount = dealerHiddenEls.length;
+
flipAllDealerHidden();
- setTimeout(()=> dealerPlay(), 300 + dealerHiddenEls.length*300);
+ setTimeout(()=> dealerPlay(), 300 + hiddenCount*250);
}
-/* ---------- 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 = [];
+ 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';
- // dealing sequence
- setTimeout(()=> dealPlayer(), 80);
- setTimeout(()=> dealDealer(false), 280); // dealer first open
- setTimeout(()=> dealPlayer(), 480);
- setTimeout(()=> dealDealer(true), 680); // dealer second hidden
+ setTimeout(()=>dealPlayer(), 100);
+ setTimeout(()=>dealDealer(false), 350);
+ setTimeout(()=>dealPlayer(), 600);
+ setTimeout(()=>dealDealer(true), 850);
- // after dealing finished, enable buttons and set phase to PLAYING
setTimeout(()=>{
gamePhase = 'PLAYING';
+ updateButtonState();
document.getElementById('status').innerText = 'YOUR TURN';
- hitBtn.disabled = false;
- standBtn.disabled = false;
- // auto-stand on natural
- if(calc(playerCards) === 21) stand();
- }, 900);
+
+ const p = calc(playerCards);
+ const playerBJ = (p === 21 && playerCards.length === 2);
+
+ if(playerBJ) stand(); // natural blackjack auto-resolve
+ }, 1000);
}
-function restart(){
- // reset and start again
- startGame();
-}
-/* ---------- events ---------- */
+/* ===========================================
+ EVENTS
+ =========================================== */
hitBtn.addEventListener('click', hit);
standBtn.addEventListener('click', stand);
-playAgainBtn.addEventListener('click', restart);
-window.addEventListener('resize', ()=>{ layoutOverlap(playerEls, cardsWrapper); layoutOverlap(dealerEls, dealerWrapper); });
-/* start immediately */
+window.addEventListener('resize', ()=>{
+ layoutOverlap(playerEls, cardsWrapper);
+ layoutOverlap(dealerEls, dealerWrapper);
+});
+
+/* START GAME ONLY ONCE */
startGame();
diff --git a/public/board.php b/public/board.php
index 6b07a80..e89e28d 100644
--- a/public/board.php
+++ b/public/board.php
@@ -1,25 +1,19 @@
prepare("SELECT b.bet, u.username FROM bets b JOIN users u ON b.uid = u.uid ORDER BY b.bet DESC LIMIT 10;");
+ $stmt = $conn->prepare("SELECT b.bet, u.username FROM bets b JOIN users u ON b.uid = u.uid ORDER BY b.bet DESC LIMIT 5;");
$stmt->execute();
$result = $stmt->get_result();
$bets = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
- // Top 10 wins
- $stmt = $conn->prepare("SELECT b.win, u.username FROM wins b JOIN users u ON b.uid = u.uid ORDER BY b.win DESC LIMIT 10;");
+ $stmt = $conn->prepare("SELECT b.win, u.username FROM wins b JOIN users u ON b.uid = u.uid ORDER BY b.win DESC LIMIT 5;");
$stmt->execute();
$result = $stmt->get_result();
$wins = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
- // Top 10 users by money
- $stmt = $conn->prepare("SELECT * FROM users ORDER BY money DESC LIMIT 10");
+ $stmt = $conn->prepare("SELECT * FROM users ORDER BY money DESC LIMIT 5");
$stmt->execute();
$result = $stmt->get_result();
$users = $result->fetch_all(MYSQLI_ASSOC);
@@ -34,94 +28,95 @@
Hit and Run — Leaderboard
-
-
-