Chotto.app
Login
Sign Up
ES
App Name
kuyawa / blackjack
App Icon in SVG
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 300 300" fill="none"> <!-- Group all elements and rotate -30 degrees around the card's center (150, 150) --> <g transform="rotate(-30 150 150)"> <!-- Main Card Body --> <rect x="75" y="45" width="150" height="210" rx="10" ry="10" fill="#FFFFFF" stroke="#000000" stroke-width="2" /> <!-- Style for all black elements (A's and Spades) --> <g fill="#000000" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> <!-- Top Left Corner 'A' and Small Spade --> <g transform="translate(85, 55)"> <text x="0" y="25" font-family="Arial, sans-serif" font-weight="bold" font-size="28" fill="#000000" stroke="none">A</text> <!-- Small Spade Icon --> <path transform="translate(0, 30) scale(1.5)" d="M 6.5 11.5 C 6.5 11.5 4.5 9 3 7 C 2 5.5 2 4 3 3 C 4 2 6 2 6.5 3 C 7 2 9 2 10 3 C 11 4 11 5.5 10 7 C 8.5 9 6.5 11.5 6.5 11.5 Z M 6 11 L 6 14 L 7 14 L 7 11 Z" fill="#000000" /> </g> <!-- Bottom Right Corner 'A' and Small Spade (Mirrored & Rotated) --> <g transform="translate(215, 245) rotate(180)"> <text x="0" y="25" font-family="Arial, sans-serif" font-weight="bold" font-size="28" fill="#000000" stroke="none">A</text> <path transform="translate(0, 30) scale(1.5)" d="M 6.5 11.5 C 6.5 11.5 4.5 9 3 7 C 2 5.5 2 4 3 3 C 4 2 6 2 6.5 3 C 7 2 9 2 10 3 C 11 4 11 5.5 10 7 C 8.5 9 6.5 11.5 6.5 11.5 Z M 6 11 L 6 14 L 7 14 L 7 11 Z" fill="#000000" /> </g> <!-- Large Center Spade Icon --> <!-- Positioned and scaled to be central and prominent --> <path transform="translate(110, 110) scale(6)" d="M 6.5 11.5 C 6.5 11.5 4.5 9 3 7 C 2 5.5 2 4 3 3 C 4 2 6 2 6.5 3 C 7 2 9 2 10 3 C 11 4 11 5.5 10 7 C 8.5 9 6.5 11.5 6.5 11.5 Z M 6 11 L 6 14 L 7 14 L 7 11 Z" fill="#000000" stroke-width="0.3" /> </g> </g> </svg>
HTML Content
<!DOCTYPE html> <html lang="en" data-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Blackjack</title> <style> /* === RESET & BASE === */ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} html{height:100%} body{ min-height:100%;display:flex;flex-direction:column; font-family:system-ui,-apple-system,'Segoe UI',sans-serif; background:var(--bg);color:var(--text); transition:background .4s,color .4s;overflow-x:hidden; -webkit-tap-highlight-color:transparent; } button{font-family:inherit;cursor:pointer;border:none;outline:none;background:none;color:inherit} button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px} button:disabled{cursor:not-allowed;opacity:.4;pointer-events:none} /* === THEME VARIABLES === */ [data-theme="dark"]{ --bg:#0a0e13;--surface:#12181f;--surface2:#1a222d; --table:#0d2e1e;--table-hi:#175a38;--table-edge:#0a2015; --card-bg:#f4efe5;--card-border:#c8c0ae; --text:#e6e0d4;--text-dim:#6b7a8c;--text-bright:#fff; --accent:#d4a843;--accent-dim:rgba(212,168,67,.2); --red-suit:#c0392b;--black-suit:#1a1a2e; --btn:#1c2a38;--btn-hover:#253848;--btn-active:#1a2530; --danger:#e05252;--success:#3ec97a;--warning:#e6a830; --overlay:rgba(0,0,0,.65); --shadow:rgba(0,0,0,.5); --chip-shadow:rgba(0,0,0,.55); --toast-bg:#1c2a38; } [data-theme="light"]{ --bg:#ddd5c8;--surface:#ece4d8;--surface2:#f5ede2; --table:#147a46;--table-hi:#1a9958;--table-edge:#0e5e34; --card-bg:#ffffff;--card-border:#d0c8b8; --text:#1a1a28;--text-dim:#6a7080;--text-bright:#000; --accent:#b08510;--accent-dim:rgba(176,133,16,.15); --red-suit:#b03020;--black-suit:#1a1a2e; --btn:#147a46;--btn-hover:#1a9958;--btn-active:#0e5e34; --danger:#c03030;--success:#1e8a4a;--warning:#c08010; --overlay:rgba(0,0,0,.35); --shadow:rgba(0,0,0,.18); --chip-shadow:rgba(0,0,0,.25); --toast-bg:#2a2a30; } /* === HEADER === */ header{ display:flex;align-items:center;justify-content:space-between; padding:12px 20px;background:var(--surface); border-bottom:1px solid rgba(128,128,128,.1); transition:background .4s;flex-shrink:0;z-index:10; } .logo{display:flex;align-items:center;gap:10px} .logo svg{width:28px;height:28px} .logo-text{font-size:20px;font-weight:700;letter-spacing:.5px;color:var(--accent)} .header-center{display:flex;align-items:center;gap:16px} .balance-box{ display:flex;align-items:center;gap:6px; background:var(--surface2);padding:6px 14px;border-radius:20px; font-variant-numeric:tabular-nums;font-size:15px;font-weight:600; transition:background .4s; } .balance-label{color:var(--text-dim);font-weight:400;font-size:13px} .balance-amount{color:var(--accent);min-width:50px;text-align:right} .header-right{display:flex;align-items:center;gap:8px} .icon-btn{ width:36px;height:36px;border-radius:50%;display:grid;place-items:center; background:var(--surface2);transition:background .2s,transform .15s; } .icon-btn:hover{background:var(--btn-hover);transform:scale(1.08)} .icon-btn svg{width:18px;height:18px;fill:var(--text)} /* === MAIN TABLE === */ main{ flex:1;display:flex;flex-direction:column; padding:12px 12px 0;min-height:0; } .table-wrap{ flex:1;border-radius:20px;position:relative;overflow:hidden; background:radial-gradient(ellipse 80% 70% at 50% 45%,var(--table-hi),var(--table) 70%,var(--table-edge)); border:3px solid var(--table-edge); box-shadow:inset 0 0 60px rgba(0,0,0,.3),0 8px 32px var(--shadow); display:flex;flex-direction:column;justify-content:space-between; padding:20px 16px;min-height:280px; transition:border-color .4s,box-shadow .4s; } /* Felt texture overlay */ .table-wrap::before{ content:'';position:absolute;inset:0;pointer-events:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Crect width='6' height='6' fill='transparent'/%3E%3Crect width='1' height='1' x='2' y='2' fill='rgba(0,0,0,0.04)'/%3E%3C/svg%3E"); opacity:.8; } /* Hand areas */ .hand-area{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:8px} .hand-label{ font-size:11px;text-transform:uppercase;letter-spacing:2px; color:rgba(255,255,255,.45);font-weight:600; } .cards-row{ display:flex;justify-content:center;align-items:center; min-height:100px;padding:4px 0; } .cards-row .card{margin-left:-14px} .cards-row .card:first-child{margin-left:0} .score-badge{ display:inline-flex;align-items:center;gap:4px; background:rgba(0,0,0,.45);backdrop-filter:blur(6px); padding:4px 12px;border-radius:12px; font-size:15px;font-weight:700;color:#fff; font-variant-numeric:tabular-nums; min-width:36px;text-align:center; transition:transform .2s; } .score-badge.bust{color:var(--danger)} .score-badge.bj{color:var(--accent);text-shadow:0 0 10px rgba(212,168,67,.5)} .score-badge.pop{animation:scorePop .3s ease} @keyframes scorePop{0%{transform:scale(1)}50%{transform:scale(1.25)}100%{transform:scale(1)}} /* Center message area */ .table-center{ position:relative;z-index:3;display:flex;align-items:center; justify-content:center;min-height:50px; } .result-overlay{ position:absolute;inset:0;z-index:5;display:grid;place-items:center; background:var(--overlay);opacity:0;pointer-events:none; transition:opacity .4s;border-radius:18px; } .result-overlay.show{opacity:1;pointer-events:auto} .result-box{text-align:center;transform:scale(.85);transition:transform .4s ease} .result-overlay.show .result-box{transform:scale(1)} .result-title{font-size:32px;font-weight:900;letter-spacing:1px;margin-bottom:4px} .result-title.win{color:var(--success)} .result-title.lose{color:var(--danger)} .result-title.push{color:var(--warning)} .result-title.bj{color:var(--accent);text-shadow:0 0 20px rgba(212,168,67,.4)} .result-sub{font-size:16px;color:rgba(255,255,255,.7);font-weight:500} /* === CARDS === */ .card{ width:80px;height:112px;flex-shrink:0;position:relative; transform-origin:center center; } .card svg{width:100%;height:100%;display:block;border-radius:7px} .card.dealing{animation:dealIn .35s ease-out both} .card.revealing{animation:flipIn .35s ease-out both} .card.flipping{animation:flipOut .3s ease-in both} @keyframes dealIn{ from{transform:translate(60px,-40px) rotate(8deg) scale(.6);opacity:0} to{transform:translate(0,0) rotate(0) scale(1);opacity:1} } @keyframes flipOut{to{transform:rotateY(90deg) scale(1.05)}} @keyframes flipIn{from{transform:rotateY(-90deg) scale(1.05)}to{transform:rotateY(0) scale(1)}} /* === CONTROLS PANEL === */ .controls{ flex-shrink:0;padding:12px 12px 16px; display:flex;flex-direction:column;gap:10px; } .bet-row{ display:flex;align-items:center;justify-content:center; gap:8px;flex-wrap:wrap; } .chips-row{display:flex;gap:6px;align-items:center} .chip{ width:48px;height:48px;border-radius:50%;position:relative; transition:transform .15s,box-shadow .15s; box-shadow:0 2px 8px var(--chip-shadow),inset 0 -2px 4px rgba(0,0,0,.2); } .chip:hover:not(:disabled){transform:translateY(-3px) scale(1.08);box-shadow:0 6px 16px var(--chip-shadow)} .chip:active:not(:disabled){transform:translateY(0) scale(.95)} .chip svg{width:100%;height:100%;display:block} .bet-display{ display:flex;align-items:center;gap:8px; background:var(--surface);padding:6px 16px;border-radius:20px; font-size:14px;font-weight:600;min-width:100px;justify-content:center; transition:background .4s; } .bet-display span{color:var(--accent)} .clear-bet{ font-size:12px;padding:6px 10px;border-radius:14px; background:var(--surface2);color:var(--text-dim); transition:background .2s,color .2s; } .clear-bet:hover{background:var(--danger);color:#fff} .actions-row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap} .action-btn{ padding:10px 22px;border-radius:10px;font-size:14px;font-weight:700; letter-spacing:.5px;text-transform:uppercase; background:var(--btn);color:var(--text); transition:background .2s,transform .1s,box-shadow .2s; box-shadow:0 2px 8px rgba(0,0,0,.2); min-width:80px; } .action-btn:hover:not(:disabled){background:var(--btn-hover);transform:translateY(-1px);box-shadow:0 4px 14px rgba(0,0,0,.3)} .action-btn:active:not(:disabled){background:var(--btn-active);transform:translateY(1px)} .action-btn.primary{background:var(--accent);color:#1a1a1a} .action-btn.primary:hover:not(:disabled){background:#e0b84e;box-shadow:0 4px 18px rgba(212,168,67,.3)} .action-btn.danger{background:rgba(224,82,82,.15);color:var(--danger)} .action-btn.danger:hover:not(:disabled){background:rgba(224,82,82,.25)} .stats-row{ display:flex;justify-content:center;gap:20px; font-size:12px;color:var(--text-dim);font-weight:500; padding-top:2px; } .stat-item{display:flex;align-items:center;gap:4px} .stat-val{font-weight:700;font-variant-numeric:tabular-nums} .stat-val.w{color:var(--success)} .stat-val.l{color:var(--danger)} .stat-val.p{color:var(--warning)} /* === TOAST === */ .toast{ position:fixed;bottom:180px;left:50%; transform:translateX(-50%) translateY(10px); background:var(--toast-bg);color:#fff; padding:8px 20px;border-radius:10px;font-size:13px;font-weight:500; opacity:0;pointer-events:none;transition:all .3s ease;z-index:100; box-shadow:0 4px 20px rgba(0,0,0,.4);white-space:nowrap; } .toast.show{opacity:1;transform:translateX(-50%) translateY(0)} /* === MODAL === */ dialog{ background:var(--surface);color:var(--text); border:1px solid rgba(128,128,128,.15);border-radius:16px; padding:28px 32px;max-width:420px;width:90%; box-shadow:0 20px 60px rgba(0,0,0,.5); transition:background .4s,color .4s; } dialog::backdrop{background:rgba(0,0,0,.6);backdrop-filter:blur(4px)} dialog h2{font-size:22px;font-weight:800;margin-bottom:14px;color:var(--accent)} dialog ul{list-style:none;display:flex;flex-direction:column;gap:8px;margin-bottom:20px} dialog li{ font-size:14px;line-height:1.5;padding-left:18px;position:relative;color:var(--text-dim); } dialog li::before{content:'';position:absolute;left:0;top:8px;width:6px;height:6px;border-radius:50%;background:var(--accent)} dialog .close-modal{ width:100%;padding:10px;border-radius:10px;font-weight:700; background:var(--btn);color:var(--text);font-size:14px; transition:background .2s; } dialog .close-modal:hover{background:var(--btn-hover)} /* === PARTICLES === */ .particle{ position:absolute;width:6px;height:6px;border-radius:50%; background:var(--accent);pointer-events:none;z-index:6; animation:particleBurst .8s ease-out forwards; } @keyframes particleBurst{ 0%{transform:translate(0,0) scale(1);opacity:1} 100%{transform:translate(var(--tx),var(--ty)) scale(0);opacity:0} } /* === GAME OVER === */ .game-over-msg{ text-align:center;padding:20px; } .game-over-msg h3{font-size:24px;font-weight:900;color:var(--danger);margin-bottom:8px} .game-over-msg p{color:var(--text-dim);margin-bottom:16px;font-size:14px} /* === RESPONSIVE === */ @media(max-width:520px){ header{padding:10px 14px} .logo-text{font-size:17px} .balance-box{padding:5px 10px;font-size:13px} .table-wrap{padding:14px 10px;min-height:240px;border-radius:14px} .card{width:64px;height:90px} .cards-row .card{margin-left:-12px} .cards-row{min-height:78px} .score-badge{font-size:13px;padding:3px 10px} .result-title{font-size:26px} .chip{width:42px;height:42px} .action-btn{padding:9px 16px;font-size:12px;min-width:65px} .bet-display{font-size:13px;padding:5px 12px;min-width:80px} .hand-label{font-size:10px;letter-spacing:1.5px} } @media(max-width:360px){ .card{width:56px;height:78px} .cards-row .card{margin-left:-10px} .chip{width:38px;height:38px} } /* === REDUCED MOTION === */ @media(prefers-reduced-motion:reduce){ *,*::before,*::after{ animation-duration:.01ms!important; animation-iteration-count:1!important; transition-duration:.01ms!important; } } </style> </head> <body> <!-- HEADER --> <header> <div class="logo"> <svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 2C16 2 6 10 6 18c0 5.5 4.5 10 10 12 5.5-2 10-6.5 10-12C26 10 16 2 16 2z" fill="var(--accent)" opacity=".9"/> <path d="M16 6C16 6 9 12 9 18c0 4 3.1 7.3 7 8.8 3.9-1.5 7-4.8 7-8.8C23 12 16 6 16 6z" fill="var(--table)"/> <path d="M16 10l2.5 5 5.5.8-4 3.9.9 5.5L16 22.2l-4.9 2.8.9-5.5-4-3.9 5.5-.8z" fill="var(--accent)" opacity=".7"/> </svg> <span class="logo-text">BLACKJACK</span> </div> <div class="header-center"> <div class="balance-box"> <span class="balance-label">Balance</span> <span class="balance-amount" id="balanceDisplay">$1,000</span> </div> </div> <div class="header-right"> <button class="icon-btn" id="infoBtn" aria-label="How to play" title="How to play"> <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg> </button> <button class="icon-btn" id="themeBtn" aria-label="Toggle theme" title="Toggle theme"> <!-- Moon icon (shown in dark mode) --> <svg id="moonIcon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> <!-- Sun icon (shown in light mode) --> <svg id="sunIcon" viewBox="0 0 24 24" style="display:none"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> </button> </div> </header> <!-- MAIN TABLE --> <main> <div class="table-wrap" id="table"> <!-- Dealer Area --> <section class="hand-area" aria-label="Dealer's hand"> <span class="hand-label">Dealer</span> <div class="cards-row" id="dealerCards"></div> <div class="score-badge" id="dealerScore" style="visibility:hidden">—</div> </section> <!-- Center --> <div class="table-center"> <div class="result-overlay" id="resultOverlay"> <div class="result-box"> <div class="result-title" id="resultTitle"></div> <div class="result-sub" id="resultSub"></div> </div> </div> </div> <!-- Player Area --> <section class="hand-area" aria-label="Player's hand"> <div class="score-badge" id="playerScore" style="visibility:hidden">—</div> <div class="cards-row" id="playerCards"></div> <span class="hand-label">You</span> </section> </div> </main> <!-- CONTROLS --> <section class="controls" aria-label="Game controls"> <div class="bet-row"> <div class="chips-row" id="chipsRow"></div> <div class="bet-display">Bet: <span id="betDisplay">$0</span></div> <button class="clear-bet" id="clearBetBtn" disabled>Clear</button> </div> <div class="actions-row" id="actionsRow"> <button class="action-btn primary" id="dealBtn" disabled>Deal</button> <button class="action-btn" id="hitBtn" disabled>Hit</button> <button class="action-btn" id="standBtn" disabled>Stand</button> <button class="action-btn danger" id="doubleBtn" disabled>Double</button> </div> <div class="stats-row" id="statsRow"> <div class="stat-item">Wins <span class="stat-val w" id="statW">0</span></div> <div class="stat-item">Losses <span class="stat-val l" id="statL">0</span></div> <div class="stat-item">Pushes <span class="stat-val p" id="statP">0</span></div> </div> </section> <!-- TOAST --> <div class="toast" id="toast" role="alert" aria-live="polite"></div> <!-- INFO MODAL --> <dialog id="infoModal"> <h2>How to Play</h2> <ul> <li>Get as close to 21 as possible without going over</li> <li>Number cards are worth their face value</li> <li>Face cards (J, Q, K) are worth 10</li> <li>Aces are worth 11 or 1, whichever is better</li> <li>Hit to draw another card</li> <li>Stand to keep your current hand</li> <li>Double Down to double your bet and receive exactly one more card</li> <li>Blackjack (Ace + 10-value on first two cards) pays 3 to 2</li> <li>Dealer must hit on 16 or less and stand on 17 or more</li> </ul> <button class="close-modal" id="closeModalBtn">Got It</button> </dialog> <script> /* === STATE === */ const state = { deck: [], playerHand: [], dealerHand: [], balance: 1000, bet: 0, lastBet: 0, phase: 'betting', // betting | dealing | playing | dealerTurn | result stats: { wins: 0, losses: 0, pushes: 0 } }; /* === DOM REFS === */ const $ = id => document.getElementById(id); const dealerCardsEl = $('dealerCards'); const playerCardsEl = $('playerCards'); const dealerScoreEl = $('dealerScore'); const playerScoreEl = $('playerScore'); const balanceEl = $('balanceDisplay'); const betEl = $('betDisplay'); const dealBtn = $('dealBtn'); const hitBtn = $('hitBtn'); const standBtn = $('standBtn'); const doubleBtn = $('doubleBtn'); const clearBetBtn = $('clearBetBtn'); const resultOverlay = $('resultOverlay'); const resultTitle = $('resultTitle'); const resultSub = $('resultSub'); const toastEl = $('toast'); const tableEl = $('table'); const chipsRow = $('chipsRow'); /* === DECK === */ const SUITS = ['spades', 'hearts', 'diamonds', 'clubs']; const SUIT_CHARS = { spades: '\u2660', hearts: '\u2665', diamonds: '\u2666', clubs: '\u2663' }; function createDeck() { const d = []; for (const s of SUITS) { for (let v = 1; v <= 13; v++) { d.push({ suit: s, value: v }); } } return d; } function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } function drawCard() { if (state.deck.length < 15) { state.deck = shuffle(createDeck()); } return state.deck.pop(); } /* === HAND CALCULATION === */ function calcHand(hand) { let value = 0, aces = 0; for (const c of hand) { if (c.value === 1) { aces++; value += 11; } else if (c.value >= 11) { value += 10; } else { value += c.value; } } let soft = aces > 0; while (value > 21 && aces > 0) { value -= 10; aces--; } if (aces === 0) soft = false; return { value, soft, bust: value > 21 }; } function isBlackjack(hand) { return hand.length === 2 && calcHand(hand).value === 21; } /* === SVG CARD RENDERING === */ function renderCardSVG(card, faceDown) { if (faceDown) { return `<div class="card card-back" role="img" aria-label="Face-down card"> <svg viewBox="0 0 90 126" xmlns="http://www.w3.org/2000/svg"> <rect width="90" height="126" rx="7" fill="#6b1515"/> <rect x="3" y="3" width="84" height="120" rx="6" fill="none" stroke="#c9a84c" stroke-width="1.2"/> <rect x="7" y="7" width="76" height="112" rx="4" fill="#7d1a1a"/> <rect x="7" y="7" width="76" height="112" rx="4" fill="none" stroke="#c9a84c" stroke-width="0.5" opacity=".6"/> <path d="M45 24 L63 63 L45 102 L27 63 Z" fill="none" stroke="#c9a84c" stroke-width="1.2" opacity=".7"/> <path d="M45 36 L55 63 L45 90 L35 63 Z" fill="none" stroke="#c9a84c" stroke-width="0.7" opacity=".5"/> <circle cx="45" cy="63" r="3.5" fill="none" stroke="#c9a84c" stroke-width="0.7" opacity=".5"/> </svg> </div>`; } const isRed = card.suit === 'hearts' || card.suit === 'diamonds'; const sc = isRed ? '#c0392b' : '#1a1a2e'; const suit = SUIT_CHARS[card.suit]; const vals = { 1: 'A', 11: 'J', 12: 'Q', 13: 'K' }; const dv = vals[card.value] || String(card.value); /* Center design varies by card type */ let center = ''; if (card.value === 1) { /* Ace: large suit symbol */ center = `<text x="45" y="72" text-anchor="middle" dominant-baseline="central" fill="${sc}" font-size="44" font-family="Georgia,serif">${suit}</text>`; } else if (card.value >= 11) { /* Face cards: decorative element + letter */ let deco = ''; if (card.value === 13) { deco = `<path d="M30 50 L35 40 L40 48 L45 36 L50 48 L55 40 L60 50" fill="none" stroke="${sc}" stroke-width="1.5" stroke-linejoin="round" opacity=".7"/>`; } else if (card.value === 12) { deco = `<path d="M33 50 L39 40 L45 46 L51 40 L57 50" fill="none" stroke="${sc}" stroke-width="1.5" stroke-linejoin="round" opacity=".7"/> <circle cx="45" cy="38" r="2" fill="${sc}" opacity=".6"/>`; } else { deco = `<path d="M45 36 L56 43 L56 60 L45 68 L34 60 L34 43 Z" fill="none" stroke="${sc}" stroke-width="1.4" opacity=".6"/>`; } center = `${deco} <text x="45" y="80" text-anchor="middle" fill="${sc}" font-size="30" font-weight="bold" font-family="Georgia,serif">${dv}</text> <text x="45" y="98" text-anchor="middle" fill="${sc}" font-size="16" font-family="serif" opacity=".7">${suit}</text>`; } else { /* Number cards: large value + suit */ center = `<text x="45" y="60" text-anchor="middle" fill="${sc}" font-size="34" font-weight="bold" font-family="Georgia,serif">${dv}</text> <text x="45" y="84" text-anchor="middle" fill="${sc}" font-size="20" font-family="serif" opacity=".7">${suit}</text>`; } return `<div class="card" role="img" aria-label="${dv} of ${card.suit}"> <svg viewBox="0 0 90 126" xmlns="http://www.w3.org/2000/svg"> <rect width="90" height="126" rx="7" fill="#f4efe5" stroke="#c8c0ae" stroke-width="0.5"/> <text x="9" y="19" fill="${sc}" font-size="15" font-weight="bold" font-family="Georgia,serif">${dv}</text> <text x="9" y="33" fill="${sc}" font-size="12" font-family="serif">${suit}</text> <g transform="translate(81,112) rotate(180)"> <text x="0" y="4" fill="${sc}" font-size="15" font-weight="bold" font-family="Georgia,serif">${dv}</text> <text x="0" y="18" fill="${sc}" font-size="12" font-family="serif">${suit}</text> </g> ${center} </svg> </div>`; } /* === CHIP RENDERING === */ const CHIP_VALUES = [5, 25, 50, 100]; const CHIP_COLORS = { 5: '#c0392b', 25: '#27ae60', 50: '#2980b9', 100: '#34495e' }; function renderChip(value) { const c = CHIP_COLORS[value]; return `<button class="chip" data-value="${value}" aria-label="Add $${value} to bet" title="Bet $${value}"> <svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg"> <circle cx="26" cy="26" r="24" fill="${c}" stroke="rgba(255,255,255,0.35)" stroke-width="2.5"/> <circle cx="26" cy="26" r="19" fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="1.8" stroke-dasharray="5.5 3.5"/> <circle cx="26" cy="26" r="14" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="1"/> <text x="26" y="30" text-anchor="middle" fill="white" font-size="13" font-weight="bold" font-family="system-ui,sans-serif" style="text-shadow:0 1px 2px rgba(0,0,0,.4)">$${value}</text> </svg> </button>`; } /* === HELPERS === */ function delay(ms) { return new Promise(r => setTimeout(r, ms)); } function formatMoney(n) { return '$' + n.toLocaleString(); } let toastTimer = null; function showToast(msg) { toastEl.textContent = msg; toastEl.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toastEl.classList.remove('show'), 2200); } /* === PARTICLES === */ function spawnParticles(count) { const rect = tableEl.getBoundingClientRect(); const cx = rect.width / 2; const cy = rect.height / 2; for (let i = 0; i < count; i++) { const p = document.createElement('div'); p.className = 'particle'; const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.4; const dist = 60 + Math.random() * 80; p.style.setProperty('--tx', Math.cos(angle) * dist + 'px'); p.style.setProperty('--ty', Math.sin(angle) * dist + 'px'); p.style.left = cx + 'px'; p.style.top = cy + 'px'; p.style.width = (4 + Math.random() * 4) + 'px'; p.style.height = p.style.width; tableEl.appendChild(p); setTimeout(() => p.remove(), 900); } } /* === UI UPDATE === */ function updateBalance() { balanceEl.textContent = formatMoney(state.balance); } function updateBet() { betEl.textContent = formatMoney(state.bet); } function updateScores(showDealer = false) { /* Player score */ if (state.playerHand.length === 0) { playerScoreEl.style.visibility = 'hidden'; } else { const p = calcHand(state.playerHand); playerScoreEl.style.visibility = 'visible'; if (p.bust) { playerScoreEl.textContent = 'BUST'; playerScoreEl.className = 'score-badge bust'; } else if (isBlackjack(state.playerHand)) { playerScoreEl.textContent = 'BJ'; playerScoreEl.className = 'score-badge bj'; } else { playerScoreEl.textContent = p.value + (p.soft ? '*' : ''); playerScoreEl.className = 'score-badge'; } popScore(playerScoreEl); } /* Dealer score */ if (state.dealerHand.length === 0) { dealerScoreEl.style.visibility = 'hidden'; } else { dealerScoreEl.style.visibility = 'visible'; if (showDealer) { const d = calcHand(state.dealerHand); if (d.bust) { dealerScoreEl.textContent = 'BUST'; dealerScoreEl.className = 'score-badge bust'; } else if (isBlackjack(state.dealerHand)) { dealerScoreEl.textContent = 'BJ'; dealerScoreEl.className = 'score-badge bj'; } else { dealerScoreEl.textContent = d.value + (d.soft ? '*' : ''); dealerScoreEl.className = 'score-badge'; } } else { /* Only show value of first (visible) card */ const first = state.dealerHand[0]; let v = first.value === 1 ? 11 : (first.value >= 11 ? 10 : first.value); dealerScoreEl.textContent = v + '+?'; dealerScoreEl.className = 'score-badge'; } popScore(dealerScoreEl); } } function popScore(el) { el.classList.remove('pop'); void el.offsetWidth; /* reflow trigger */ el.classList.add('pop'); } function updateStats() { $('statW').textContent = state.stats.wins; $('statL').textContent = state.stats.losses; $('statP').textContent = state.stats.pushes; } function updateControls() { const { phase, bet, balance, playerHand } = state; /* Chips */ document.querySelectorAll('.chip').forEach(c => { c.disabled = phase !== 'betting'; }); /* Clear bet */ clearBetBtn.disabled = phase !== 'betting' || bet === 0; /* Deal / New Hand */ if (phase === 'betting') { dealBtn.textContent = 'Deal'; dealBtn.disabled = bet === 0; dealBtn.style.display = ''; } else if (phase === 'result') { dealBtn.textContent = 'New Hand'; dealBtn.disabled = false; dealBtn.style.display = ''; } else { dealBtn.disabled = true; } /* Action buttons */ const canDouble = phase === 'playing' && playerHand.length === 2 && balance >= bet; hitBtn.disabled = phase !== 'playing'; standBtn.disabled = phase !== 'playing'; doubleBtn.disabled = !canDouble; } function clearTable() { dealerCardsEl.innerHTML = ''; playerCardsEl.innerHTML = ''; dealerScoreEl.style.visibility = 'hidden'; playerScoreEl.style.visibility = 'hidden'; resultOverlay.classList.remove('show'); } function addCardToUI(target, card, faceDown) { const container = target === 'player' ? playerCardsEl : dealerCardsEl; const div = document.createElement('div'); div.innerHTML = renderCardSVG(card, faceDown); const cardEl = div.firstElementChild; cardEl.classList.add('dealing'); container.appendChild(cardEl); } /* === GAME FLOW === */ async function startDeal() { if (state.bet === 0) return; state.phase = 'dealing'; state.playerHand = []; state.dealerHand = []; clearTable(); updateControls(); /* Deduct bet from balance */ state.balance -= state.bet; updateBalance(); /* Deal 4 cards sequentially */ const c1 = drawCard(); state.playerHand.push(c1); addCardToUI('player', c1, false); updateScores(false); await delay(280); const c2 = drawCard(); state.dealerHand.push(c2); addCardToUI('dealer', c2, false); updateScores(false); await delay(280); const c3 = drawCard(); state.playerHand.push(c3); addCardToUI('player', c3, false); updateScores(false); await delay(280); const c4 = drawCard(); state.dealerHand.push(c4); addCardToUI('dealer', c4, true); /* Hole card face down */ updateScores(false); await delay(300); /* Check for blackjacks */ const playerBJ = isBlackjack(state.playerHand); const dealerBJ = isBlackjack(state.dealerHand); if (playerBJ || dealerBJ) { await revealHoleCard(); await delay(400); if (playerBJ && dealerBJ) { endRound('push', 'Both have Blackjack'); } else if (playerBJ) { endRound('blackjack', 'Blackjack!'); } else { endRound('dealer', 'Dealer has Blackjack'); } return; } state.phase = 'playing'; updateControls(); } async function playerHit() { if (state.phase !== 'playing') return; const card = drawCard(); state.playerHand.push(card); addCardToUI('player', card, false); updateScores(false); await delay(250); const p = calcHand(state.playerHand); if (p.bust) { await delay(300); await revealHoleCard(); endRound('dealer', 'You busted'); } else if (p.value === 21) { /* Auto-stand on 21 */ await playerStand(); } updateControls(); } async function playerStand() { if (state.phase !== 'playing') return; state.phase = 'dealerTurn'; updateControls(); await revealHoleCard(); await delay(500); /* Dealer draws until 17+ */ while (calcHand(state.dealerHand).value < 17) { const card = drawCard(); state.dealerHand.push(card); addCardToUI('dealer', card, false); updateScores(true); await delay(550); } await delay(300); /* Determine winner */ const pv = calcHand(state.playerHand).value; const dv = calcHand(state.dealerHand).value; const dBust = calcHand(state.dealerHand).bust; if (dBust) { endRound('player', 'Dealer busted'); } else if (dv > pv) { endRound('dealer', 'Dealer wins'); } else if (pv > dv) { endRound('player', 'You win'); } else { endRound('push', 'Push'); } } async function playerDouble() { if (state.phase !== 'playing') return; if (state.playerHand.length !== 2) return; if (state.balance < state.bet) { showToast('Not enough balance to double'); return; } /* Double the bet */ state.balance -= state.bet; state.bet *= 2; updateBalance(); updateBet(); state.phase = 'dealerTurn'; updateControls(); /* Draw exactly one card */ const card = drawCard(); state.playerHand.push(card); addCardToUI('player', card, false); updateScores(false); await delay(350); const p = calcHand(state.playerHand); if (p.bust) { await revealHoleCard(); await delay(300); endRound('dealer', 'You busted'); return; } /* Then dealer plays */ await revealHoleCard(); await delay(500); while (calcHand(state.dealerHand).value < 17) { const c = drawCard(); state.dealerHand.push(c); addCardToUI('dealer', c, false); updateScores(true); await delay(550); } await delay(300); const pv = calcHand(state.playerHand).value; const dv = calcHand(state.dealerHand).value; const dBust = calcHand(state.dealerHand).bust; if (dBust) { endRound('player', 'Dealer busted'); } else if (dv > pv) { endRound('dealer', 'Dealer wins'); } else if (pv > dv) { endRound('player', 'You win'); } else { endRound('push', 'Push'); } } async function revealHoleCard() { const holeCard = dealerCardsEl.querySelector('.card-back'); if (!holeCard) { updateScores(true); return; } holeCard.classList.add('flipping'); await delay(300); /* Replace with face-up card */ const card = state.dealerHand[1]; const temp = document.createElement('div'); temp.innerHTML = renderCardSVG(card, false); const newCard = temp.firstElementChild; newCard.classList.add('revealing'); holeCard.replaceWith(newCard); updateScores(true); await delay(350); } function endRound(outcome, message) { state.phase = 'result'; let payout = 0; switch (outcome) { case 'blackjack': payout = state.bet + Math.floor(state.bet * 1.5); state.stats.wins++; resultTitle.textContent = message; resultTitle.className = 'result-title bj'; resultSub.textContent = '+' + formatMoney(payout - state.bet); spawnParticles(24); break; case 'player': payout = state.bet * 2; state.stats.wins++; resultTitle.textContent = message; resultTitle.className = 'result-title win'; resultSub.textContent = '+' + formatMoney(state.bet); spawnParticles(16); break; case 'dealer': payout = 0; state.stats.losses++; resultTitle.textContent = message; resultTitle.className = 'result-title lose'; resultSub.textContent = '-' + formatMoney(state.bet); break; case 'push': payout = state.bet; state.stats.pushes++; resultTitle.textContent = message; resultTitle.className = 'result-title push'; resultSub.textContent = 'Bet returned'; break; } state.balance += payout; state.bet = 0; updateBalance(); updateBet(); updateStats(); updateControls(); /* Check game over */ if (state.balance <= 0) { setTimeout(() => { resultSub.textContent = ''; resultTitle.textContent = 'Game Over'; resultTitle.className = 'result-title lose'; const goMsg = document.createElement('p'); goMsg.style.cssText = 'color:rgba(255,255,255,.6);margin-top:8px;font-size:14px'; goMsg.textContent = 'You ran out of chips.'; resultSub.after(goMsg); const resetBtn = document.createElement('button'); resetBtn.className = 'action-btn primary'; resetBtn.style.cssText = 'margin-top:14px;font-size:13px;padding:8px 24px'; resetBtn.textContent = 'Start Fresh ($1,000)'; resetBtn.onclick = () => { state.balance = 1000; state.stats = { wins: 0, losses: 0, pushes: 0 }; updateBalance(); updateStats(); newHand(); resetBtn.remove(); goMsg.remove(); }; goMsg.after(resetBtn); }, 600); } resultOverlay.classList.add('show'); } function newHand() { state.phase = 'betting'; state.bet = 0; state.playerHand = []; state.dealerHand = []; clearTable(); updateBet(); updateControls(); } /* === BETTING === */ function addBet(amount) { if (state.phase !== 'betting') return; if (amount > state.balance - state.bet) { showToast('Not enough balance'); return; } state.bet += amount; state.lastBet = state.bet; updateBet(); updateControls(); } function clearBet() { if (state.phase !== 'betting') return; state.bet = 0; updateBet(); updateControls(); } /* === THEME === */ function toggleTheme() { const html = document.documentElement; const isDark = html.getAttribute('data-theme') === 'dark'; html.setAttribute('data-theme', isDark ? 'light' : 'dark'); $('moonIcon').style.display = isDark ? 'none' : ''; $('sunIcon').style.display = isDark ? '' : 'none'; try { localStorage.setItem('bj-theme', isDark ? 'light' : 'dark'); } catch(e) {} } function loadTheme() { try { const saved = localStorage.getItem('bj-theme'); if (saved) { document.documentElement.setAttribute('data-theme', saved); $('moonIcon').style.display = saved === 'dark' ? '' : 'none'; $('sunIcon').style.display = saved === 'dark' ? 'none' : ''; } } catch(e) {} } /* === KEYBOARD SHORTCUTS === */ function handleKeydown(e) { if ($('infoModal').open) return; const key = e.key.toLowerCase(); console.log('key', key) if (key === 'h' && state.phase === 'playing') { e.preventDefault(); playerHit(); } else if (key === '1' && state.phase === 'betting') { e.preventDefault(); addBet(5); } else if (key === '2' && state.phase === 'betting') { e.preventDefault(); addBet(25); } else if (key === '3' && state.phase === 'betting') { e.preventDefault(); addBet(50); } else if (key === '4' && state.phase === 'betting') { e.preventDefault(); addBet(100); } else if (key === 's' && state.phase === 'playing') { e.preventDefault(); playerStand(); } else if (key === 'd' && state.phase === 'playing') { e.preventDefault(); playerDouble(); } else if ((key === ' ' || key === 'enter') && (state.phase === 'betting' || state.phase === 'result')) { e.preventDefault(); if (state.phase === 'betting' && state.bet > 0) startDeal(); else if (state.phase === 'result'){ newHand(); addBet(state.lastBet); startDeal(); } } } /* === INIT === */ function init() { /* Create deck */ state.deck = shuffle(createDeck()); /* Render chips */ chipsRow.innerHTML = CHIP_VALUES.map(v => renderChip(v)).join(''); /* Wire chip clicks */ chipsRow.addEventListener('click', e => { const chip = e.target.closest('.chip'); if (!chip || chip.disabled) return; addBet(parseInt(chip.dataset.value)); }); /* Wire action buttons */ dealBtn.addEventListener('click', () => { if (state.phase === 'betting') startDeal(); else if (state.phase === 'result') newHand(); }); hitBtn.addEventListener('click', playerHit); standBtn.addEventListener('click', playerStand); doubleBtn.addEventListener('click', playerDouble); clearBetBtn.addEventListener('click', clearBet); /* Theme */ $('themeBtn').addEventListener('click', toggleTheme); loadTheme(); /* Info modal */ $('infoBtn').addEventListener('click', () => $('infoModal').showModal()); $('closeModalBtn').addEventListener('click', () => $('infoModal').close()); $('infoModal').addEventListener('click', e => { if (e.target === $('infoModal')) $('infoModal').close(); }); /* Keyboard */ document.addEventListener('keydown', handleKeydown); /* Initial UI state */ updateBalance(); updateBet(); updateControls(); updateStats(); } init(); </script> </body> </html>
Cancel