← back to Model Arena
Run on Claude Max subscription only: drop paid API models (kimi/gpt/grok) from arena roster — claude-code (Max plan) + 5 local models = $0. Agent-driven builds already on Max plan.
682fd4679864aa06d07922d98dbce100d7195d23 · 2026-07-25 08:52:21 -0700 · Steve Abrams
Files touched
M idea-run/run-next.shA staging-90210/90210-blackjack.html
Diff
commit 682fd4679864aa06d07922d98dbce100d7195d23
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 08:52:21 2026 -0700
Run on Claude Max subscription only: drop paid API models (kimi/gpt/grok) from arena roster — claude-code (Max plan) + 5 local models = $0. Agent-driven builds already on Max plan.
---
idea-run/run-next.sh | 2 +-
staging-90210/90210-blackjack.html | 1354 ++++++++++++++++++++++++++++++++++++
2 files changed, 1355 insertions(+), 1 deletion(-)
diff --git a/idea-run/run-next.sh b/idea-run/run-next.sh
index d28adcc..8db3e7e 100755
--- a/idea-run/run-next.sh
+++ b/idea-run/run-next.sh
@@ -21,7 +21,7 @@ PROMPT="$PROMPT DW LUXURY UI REQUIREMENTS (mandatory): (a) a fixed top bar with
python3 - "$TITLE" "$PROMPT" > /tmp/dwaa-payload.json <<'PY'
import json,sys
json.dump({"title":sys.argv[1],"prompt":sys.argv[2],
- "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code","kimi","gpt","grok"],"designTools":True},
+ "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code"],"designTools":True},
sys.stdout)
PY
diff --git a/staging-90210/90210-blackjack.html b/staging-90210/90210-blackjack.html
new file mode 100644
index 0000000..d3d1db8
--- /dev/null
+++ b/staging-90210/90210-blackjack.html
@@ -0,0 +1,1354 @@
+<!DOCTYPE html>
+<!--
+ 90210 BLACKJACK — PLAN
+ ======================
+ Single-deck blackjack vs. house dealer.
+
+ RULES:
+ - Standard single deck (52 cards), reshuffled when below 15 cards remain.
+ - Player: hit, stand, double down (first two cards only, costs extra bet from bankroll).
+ - Dealer: hits on soft 16 or less, stands on hard/soft 17+.
+ - Aces count as 11 unless that would bust, then 1.
+ - Blackjack (natural 21 on first two cards) pays 3:2.
+ - Double down: bet doubled, exactly one more card drawn.
+ - Push (tie): bet returned.
+ - No split, no insurance (keeps logic clean and game tight).
+
+ STATE MACHINE: idle → betting → dealing → player-turn → dealer-turn → result
+ - Bet is placed by clicking chip buttons before deal.
+ - Card deal is animated via CSS transform (fly-in from deck position).
+ - Dealer's hole card is face-down until dealer's turn.
+ - Bankroll + best win streak persisted in localStorage.
+
+ SOUND: Web Audio API — card deal tick, chip clink, win fanfare, lose thud,
+ all procedurally generated. Mute toggle top-right.
+
+ UI: Gold-on-marble table. No CDN, no external assets. Everything inline.
+ Serif display font via system stack: "Cormorant Garamond", Georgia, serif.
+ Sans UI font: "Gill Sans", "Optima", sans-serif.
+ Colors: --gold #c9a961, --champagne #f0e6c8, --marble #f7f3ed,
+ --felt #1a3a2a (table felt), --ink #1a1209, --cream #faf6ef.
+-->
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>90210 Blackjack — Beverly Hills Casino</title>
+<style>
+:root {
+ --gold: #c9a961;
+ --gold-light: #e8d49a;
+ --gold-dark: #9a7a3a;
+ --champagne: #f0e6c8;
+ --marble: #f7f3ed;
+ --marble2: #ede8e0;
+ --felt: #1c3d2a;
+ --felt-dark: #122a1c;
+ --felt-rail: #0f2018;
+ --ink: #1a1209;
+ --cream: #faf6ef;
+ --red-card: #8b1a1a;
+ --serif: "Cormorant Garamond", "Garamond", "Times New Roman", Georgia, serif;
+ --sans: "Gill Sans", "Optima", "Trebuchet MS", system-ui, sans-serif;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ background: #0e1a14;
+ font-family: var(--sans);
+ color: var(--cream);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-x: hidden;
+ user-select: none;
+}
+
+/* ─── HEADER ─── */
+header {
+ width: 100%;
+ text-align: center;
+ padding: 18px 20px 12px;
+ border-bottom: 1px solid rgba(201,169,97,0.25);
+ position: relative;
+ background: linear-gradient(180deg, #0a110d 0%, #0e1a14 100%);
+}
+
+.wordmark {
+ font-family: var(--serif);
+ font-size: clamp(22px, 4vw, 34px);
+ letter-spacing: 0.25em;
+ color: var(--gold);
+ text-transform: uppercase;
+ font-weight: 400;
+}
+
+.wordmark span { color: var(--champagne); }
+
+.tagline {
+ font-family: var(--serif);
+ font-size: clamp(10px, 1.6vw, 13px);
+ letter-spacing: 0.4em;
+ color: rgba(201,169,97,0.55);
+ text-transform: uppercase;
+ margin-top: 3px;
+}
+
+#muteBtn {
+ position: absolute;
+ right: 16px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(201,169,97,0.12);
+ border: 1px solid rgba(201,169,97,0.3);
+ color: var(--gold);
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ font-size: 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+}
+#muteBtn:hover { background: rgba(201,169,97,0.22); }
+
+/* ─── BANKROLL BAR ─── */
+.bankroll-bar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 32px;
+ padding: 10px 20px;
+ font-size: clamp(11px, 2vw, 13px);
+ letter-spacing: 0.12em;
+ color: rgba(240,230,200,0.7);
+ border-bottom: 1px solid rgba(201,169,97,0.1);
+}
+
+.bankroll-bar .val {
+ font-family: var(--serif);
+ font-size: clamp(15px, 2.5vw, 20px);
+ color: var(--gold);
+ letter-spacing: 0.05em;
+}
+
+/* ─── TABLE ─── */
+.table-wrap {
+ flex: 1;
+ width: 100%;
+ max-width: 900px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0 16px 20px;
+}
+
+.felt-table {
+ width: 100%;
+ background: radial-gradient(ellipse 80% 70% at 50% 45%, #26563a 0%, #1c3d2a 50%, #122a1c 100%);
+ border-radius: 16px;
+ border: 3px solid var(--gold-dark);
+ box-shadow:
+ 0 0 0 6px #0a1a10,
+ 0 0 0 8px var(--gold-dark),
+ inset 0 2px 8px rgba(0,0,0,0.5),
+ 0 20px 60px rgba(0,0,0,0.7);
+ padding: 20px 24px 24px;
+ margin-top: 16px;
+ position: relative;
+ min-height: 400px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+/* table rail ornament */
+.felt-table::before {
+ content: '';
+ position: absolute;
+ inset: 8px;
+ border-radius: 10px;
+ border: 1px solid rgba(201,169,97,0.15);
+ pointer-events: none;
+}
+
+.zone-label {
+ font-family: var(--serif);
+ font-size: clamp(9px, 1.5vw, 11px);
+ letter-spacing: 0.35em;
+ color: rgba(201,169,97,0.35);
+ text-transform: uppercase;
+ text-align: center;
+ margin-bottom: 4px;
+}
+
+/* ─── CARD ZONES ─── */
+.hand-zone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ flex: 1;
+}
+
+.hand-row {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 120px;
+ gap: 0;
+ position: relative;
+}
+
+/* ─── CARDS ─── */
+.card {
+ width: clamp(56px, 8vw, 72px);
+ height: clamp(80px, 11.4vw, 102px);
+ border-radius: 6px;
+ border: 1.5px solid rgba(180,160,120,0.3);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 4px 5px;
+ font-family: var(--serif);
+ font-size: clamp(14px, 2.5vw, 18px);
+ position: relative;
+ margin-left: -14px;
+ transition: transform 0.15s ease;
+ flex-shrink: 0;
+ box-shadow: 2px 3px 8px rgba(0,0,0,0.5);
+
+ /* animation origin */
+ transform-origin: center bottom;
+ animation: cardDeal 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+.card:first-child { margin-left: 0; }
+
+@keyframes cardDeal {
+ from {
+ opacity: 0;
+ transform: translateY(-40px) scale(0.7) rotate(-8deg);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1) rotate(0deg);
+ }
+}
+
+.card.face-up {
+ background: linear-gradient(145deg, #fdfaf4 0%, #f5efe3 100%);
+ color: var(--ink);
+}
+
+.card.face-down {
+ background:
+ repeating-linear-gradient(
+ 45deg,
+ #1a3d2a 0px, #1a3d2a 3px,
+ #15321f 3px, #15321f 6px
+ );
+ border-color: rgba(201,169,97,0.4);
+}
+
+.card.face-down::after {
+ content: '';
+ position: absolute;
+ inset: 4px;
+ border: 1px solid rgba(201,169,97,0.25);
+ border-radius: 3px;
+}
+
+.card-corner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ line-height: 1;
+}
+
+.card-corner.bottom {
+ transform: rotate(180deg);
+}
+
+.card-rank {
+ font-size: inherit;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.card-suit {
+ font-size: 0.7em;
+ line-height: 1;
+ margin-top: 1px;
+}
+
+.card.red .card-rank,
+.card.red .card-suit { color: var(--red-card); }
+
+.card-center {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: clamp(20px, 3.5vw, 28px);
+ opacity: 0.18;
+}
+.card.red .card-center { color: var(--red-card); }
+
+/* ─── SCORE BADGE ─── */
+.score-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0,0,0,0.45);
+ border: 1px solid rgba(201,169,97,0.3);
+ border-radius: 20px;
+ padding: 2px 12px;
+ font-family: var(--serif);
+ font-size: clamp(13px, 2vw, 16px);
+ color: var(--champagne);
+ letter-spacing: 0.08em;
+ min-width: 36px;
+ text-align: center;
+ min-height: 24px;
+}
+
+.score-badge.bust { color: #e07070; border-color: rgba(200,80,80,0.4); }
+.score-badge.blackjack { color: var(--gold); border-color: rgba(201,169,97,0.6); }
+.score-badge.good { color: #7dd4a0; border-color: rgba(80,180,100,0.35); }
+
+/* ─── DIVIDER ─── */
+.table-divider {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 4px 0;
+ color: rgba(201,169,97,0.25);
+ font-size: 11px;
+ letter-spacing: 0.3em;
+}
+.table-divider::before, .table-divider::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: rgba(201,169,97,0.15);
+}
+
+/* ─── BET AREA ─── */
+.bet-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 0;
+}
+
+.current-bet-display {
+ font-family: var(--serif);
+ font-size: clamp(18px, 3vw, 26px);
+ color: var(--gold);
+ letter-spacing: 0.08em;
+ min-height: 32px;
+ text-align: center;
+}
+
+.chip-row {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.chip {
+ width: clamp(44px, 7vw, 56px);
+ height: clamp(44px, 7vw, 56px);
+ border-radius: 50%;
+ border: 3px dashed rgba(255,255,255,0.35);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--serif);
+ font-size: clamp(11px, 1.8vw, 14px);
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.1s ease, box-shadow 0.15s ease;
+ position: relative;
+ box-shadow: 0 3px 8px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,255,0.15);
+}
+
+.chip:hover { transform: scale(1.08); }
+.chip:active { transform: scale(0.96); }
+
+.chip.c5 { background: radial-gradient(circle at 35% 35%, #e05050, #8b1a1a); color: #fde8e8; }
+.chip.c10 { background: radial-gradient(circle at 35% 35%, #5080e0, #1a3a8b); color: #e8eefe; }
+.chip.c25 { background: radial-gradient(circle at 35% 35%, #50b050, #1a5a1a); color: #e8fde8; }
+.chip.c50 { background: radial-gradient(circle at 35% 35%, #c9a961, #7a5a1a); color: #fdf0d0; }
+.chip.c100 { background: radial-gradient(circle at 35% 35%, #2a2a2a, #111); color: var(--gold); border-color: rgba(201,169,97,0.5); }
+.chip.clear { background: radial-gradient(circle at 35% 35%, #555, #222); color: #aaa; font-size: clamp(9px, 1.4vw, 11px); border-color: rgba(255,255,255,0.15); }
+
+/* ─── ACTION BUTTONS ─── */
+.action-row {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ flex-wrap: wrap;
+ padding: 6px 0;
+}
+
+.action-btn {
+ font-family: var(--serif);
+ font-size: clamp(13px, 2vw, 16px);
+ letter-spacing: 0.15em;
+ padding: clamp(9px, 2vw, 12px) clamp(18px, 4vw, 32px);
+ border-radius: 4px;
+ border: none;
+ cursor: pointer;
+ text-transform: uppercase;
+ transition: transform 0.1s ease, box-shadow 0.15s ease, opacity 0.2s;
+ position: relative;
+ overflow: hidden;
+}
+
+.action-btn:active { transform: scale(0.96); }
+.action-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--gold-light) 0%, var(--gold) 50%, var(--gold-dark) 100%);
+ color: var(--ink);
+ box-shadow: 0 3px 14px rgba(201,169,97,0.4);
+}
+.btn-primary:hover:not(:disabled) { box-shadow: 0 5px 20px rgba(201,169,97,0.6); }
+
+.btn-secondary {
+ background: rgba(201,169,97,0.1);
+ color: var(--gold);
+ border: 1px solid rgba(201,169,97,0.4);
+}
+.btn-secondary:hover:not(:disabled) { background: rgba(201,169,97,0.18); }
+
+.btn-danger {
+ background: rgba(160,50,50,0.15);
+ color: #e08888;
+ border: 1px solid rgba(180,60,60,0.35);
+}
+.btn-danger:hover:not(:disabled) { background: rgba(160,50,50,0.25); }
+
+/* ─── STATUS MESSAGE ─── */
+.status-msg {
+ font-family: var(--serif);
+ font-size: clamp(14px, 2.5vw, 18px);
+ letter-spacing: 0.12em;
+ color: var(--champagne);
+ text-align: center;
+ min-height: 26px;
+ padding: 4px 0;
+}
+
+/* ─── RESULT OVERLAY ─── */
+#resultOverlay {
+ position: absolute;
+ inset: 0;
+ border-radius: 13px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ z-index: 10;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.35s ease;
+ backdrop-filter: blur(2px);
+}
+
+#resultOverlay.show {
+ opacity: 1;
+ pointer-events: all;
+}
+
+.result-card {
+ background: rgba(10,18,13,0.92);
+ border: 2px solid var(--gold);
+ border-radius: 12px;
+ padding: 24px 40px;
+ text-align: center;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.8), 0 0 40px rgba(201,169,97,0.15);
+ animation: resultPop 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes resultPop {
+ from { transform: scale(0.7); opacity: 0; }
+ to { transform: scale(1); opacity: 1; }
+}
+
+.result-headline {
+ font-family: var(--serif);
+ font-size: clamp(28px, 5vw, 44px);
+ color: var(--gold);
+ letter-spacing: 0.1em;
+ line-height: 1;
+ margin-bottom: 6px;
+}
+
+.result-headline.lose { color: #e07070; }
+.result-headline.push { color: var(--champagne); }
+
+.result-sub {
+ font-family: var(--serif);
+ font-size: clamp(14px, 2vw, 18px);
+ color: rgba(240,230,200,0.7);
+ letter-spacing: 0.15em;
+ margin-bottom: 14px;
+}
+
+.result-amount {
+ font-family: var(--serif);
+ font-size: clamp(20px, 3.5vw, 30px);
+ letter-spacing: 0.05em;
+ margin-bottom: 18px;
+}
+
+.result-amount.win { color: #80d890; }
+.result-amount.lose { color: #e07070; }
+.result-amount.push { color: var(--champagne); }
+
+/* ─── FOOTER ─── */
+footer {
+ width: 100%;
+ text-align: center;
+ padding: 10px 20px;
+ font-size: 10px;
+ letter-spacing: 0.25em;
+ color: rgba(201,169,97,0.25);
+ text-transform: uppercase;
+ border-top: 1px solid rgba(201,169,97,0.08);
+}
+
+/* ─── STREAK BADGE ─── */
+.streak-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ background: rgba(201,169,97,0.1);
+ border: 1px solid rgba(201,169,97,0.25);
+ border-radius: 20px;
+ padding: 2px 10px;
+ font-size: clamp(10px, 1.5vw, 12px);
+ color: rgba(240,230,200,0.65);
+ letter-spacing: 0.1em;
+}
+
+/* ─── RESPONSIVE ─── */
+@media (max-width: 480px) {
+ .chip-row { gap: 7px; }
+ .action-row { gap: 7px; }
+ .action-btn { padding: 9px 16px; }
+}
+
+/* ─── BUST FLASH ─── */
+@keyframes bustFlash {
+ 0%, 100% { box-shadow: inset 0 0 0 transparent; }
+ 50% { box-shadow: inset 0 0 30px rgba(200,60,60,0.4); }
+}
+.bust-flash { animation: bustFlash 0.5s ease 2; }
+
+/* ─── CARD FLIP (hole card reveal) ─── */
+@keyframes flipReveal {
+ 0% { transform: rotateY(90deg) scale(0.9); opacity: 0; }
+ 100% { transform: rotateY(0deg) scale(1); opacity: 1; }
+}
+.flip-reveal { animation: flipReveal 0.3s ease forwards; }
+
+/* ─── BLACKJACK SHIMMER ─── */
+@keyframes bjShimmer {
+ 0%, 100% { border-color: var(--gold-dark); }
+ 50% { border-color: var(--gold-light); box-shadow: 0 0 16px rgba(201,169,97,0.6); }
+}
+.bj-shimmer { animation: bjShimmer 0.6s ease 3; }
+
+/* ─── NO SELECT ─── */
+.card, .chip, .action-btn { -webkit-tap-highlight-color: transparent; }
+</style>
+</head>
+<body>
+
+<header>
+ <div class="wordmark">90210 <span>Blackjack</span></div>
+ <div class="tagline">Beverly Hills Casino Royale</div>
+ <button id="muteBtn" title="Toggle sound" aria-label="Toggle sound">🔊</button>
+</header>
+
+<div class="bankroll-bar">
+ <div>
+ <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">BANKROLL</div>
+ <div class="val" id="bankrollDisplay">$1,000</div>
+ </div>
+ <div>
+ <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">CURRENT BET</div>
+ <div class="val" id="currentBetDisplay">—</div>
+ </div>
+ <div>
+ <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">BEST WIN</div>
+ <div class="val" id="bestWinDisplay">$0</div>
+ </div>
+ <div class="streak-badge" id="streakBadge" style="display:none">
+ <span>🔥</span><span id="streakVal">0</span><span>streak</span>
+ </div>
+</div>
+
+<div class="table-wrap">
+ <div class="felt-table" id="feltTable">
+ <div id="resultOverlay">
+ <div class="result-card" id="resultCard">
+ <div class="result-headline" id="resultHeadline">WIN</div>
+ <div class="result-sub" id="resultSub">Blackjack!</div>
+ <div class="result-amount" id="resultAmount">+$150</div>
+ <button class="action-btn btn-primary" id="nextHandBtn" style="letter-spacing:0.2em">DEAL AGAIN</button>
+ </div>
+ </div>
+
+ <!-- Dealer zone -->
+ <div class="hand-zone">
+ <div class="zone-label">Dealer</div>
+ <div class="hand-row" id="dealerHand"></div>
+ <div class="score-badge" id="dealerScore" style="visibility:hidden">0</div>
+ </div>
+
+ <div class="table-divider">⟡ ⟡ ⟡</div>
+
+ <!-- Status -->
+ <div class="status-msg" id="statusMsg">Place your bet to begin</div>
+
+ <!-- Bet / action area -->
+ <div class="bet-area" id="betArea">
+ <div class="current-bet-display" id="betDisplay"></div>
+ <div class="chip-row" id="chipRow">
+ <button class="chip c5" data-val="5" aria-label="$5 chip">$5</button>
+ <button class="chip c10" data-val="10" aria-label="$10 chip">$10</button>
+ <button class="chip c25" data-val="25" aria-label="$25 chip">$25</button>
+ <button class="chip c50" data-val="50" aria-label="$50 chip">$50</button>
+ <button class="chip c100" data-val="100" aria-label="$100 chip">$100</button>
+ <button class="chip clear" id="clearBetBtn" aria-label="Clear bet">CLEAR</button>
+ </div>
+ </div>
+
+ <div class="action-row" id="actionRow" style="display:none">
+ <button class="action-btn btn-primary" id="hitBtn" disabled>Hit</button>
+ <button class="action-btn btn-secondary" id="standBtn" disabled>Stand</button>
+ <button class="action-btn btn-secondary" id="doubleBtn" disabled>Double</button>
+ </div>
+
+ <!-- Player zone -->
+ <div class="hand-zone" style="flex-direction:column-reverse">
+ <div class="zone-label" style="margin-bottom:0;margin-top:4px">You</div>
+ <div class="score-badge" id="playerScore" style="visibility:hidden">0</div>
+ <div class="hand-row" id="playerHand"></div>
+ </div>
+
+ <!-- Deal button (shown when not playing) -->
+ <div class="action-row" id="dealRow">
+ <button class="action-btn btn-primary" id="dealBtn" disabled>Deal</button>
+ <button class="action-btn btn-danger" id="resetBtn" title="Reset bankroll to $1,000">↺ Reset</button>
+ </div>
+
+ </div>
+</div>
+
+<footer>
+ 90210 Blackjack · Single Deck · Dealer Stands on 17 · Blackjack Pays 3:2 · Beverly Hills Casino
+</footer>
+
+<script>
+// ═══════════════════════════════════════════════════════
+// AUDIO ENGINE — procedural Web Audio
+// ═══════════════════════════════════════════════════════
+const AudioCtx = window.AudioContext || window.webkitAudioContext;
+let ctx = null;
+let muted = false;
+
+function getCtx() {
+ if (!ctx) ctx = new AudioCtx();
+ if (ctx.state === 'suspended') ctx.resume();
+ return ctx;
+}
+
+function playTone({ freq = 440, type = 'sine', vol = 0.25, dur = 0.12, attack = 0.01, decay = 0.05, detune = 0 } = {}) {
+ if (muted) return;
+ try {
+ const c = getCtx();
+ const osc = c.createOscillator();
+ const gain = c.createGain();
+ osc.connect(gain); gain.connect(c.destination);
+ osc.type = type;
+ osc.frequency.value = freq;
+ osc.detune.value = detune;
+ gain.gain.setValueAtTime(0, c.currentTime);
+ gain.gain.linearRampToValueAtTime(vol, c.currentTime + attack);
+ gain.gain.linearRampToValueAtTime(0, c.currentTime + dur);
+ osc.start(c.currentTime);
+ osc.stop(c.currentTime + dur + 0.02);
+ } catch(e) {}
+}
+
+function playNoise(vol = 0.08, dur = 0.06) {
+ if (muted) return;
+ try {
+ const c = getCtx();
+ const len = Math.floor(c.sampleRate * dur);
+ const buf = c.createBuffer(1, len, c.sampleRate);
+ const d = buf.getChannelData(0);
+ for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
+ const src = c.createBufferSource();
+ src.buffer = buf;
+ const g = c.createGain();
+ g.gain.setValueAtTime(vol, c.currentTime);
+ g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur);
+ const filt = c.createBiquadFilter();
+ filt.type = 'bandpass';
+ filt.frequency.value = 1200;
+ filt.Q.value = 2;
+ src.connect(filt); filt.connect(g); g.connect(c.destination);
+ src.start(); src.stop(c.currentTime + dur + 0.02);
+ } catch(e) {}
+}
+
+const sounds = {
+ deal() {
+ playNoise(0.12, 0.04);
+ setTimeout(() => playTone({ freq: 900, type: 'triangle', vol: 0.08, dur: 0.08 }), 20);
+ },
+ chip() {
+ playNoise(0.1, 0.03);
+ playTone({ freq: 1800, type: 'sine', vol: 0.06, dur: 0.06, attack: 0.005 });
+ },
+ win() {
+ [0, 80, 200, 360].forEach((ms, i) => {
+ setTimeout(() => {
+ const freqs = [523, 659, 784, 1047];
+ playTone({ freq: freqs[i], type: 'triangle', vol: 0.18 - i*0.02, dur: 0.25, attack: 0.01 });
+ }, ms);
+ });
+ },
+ blackjack() {
+ [0, 100, 220, 360, 520].forEach((ms, i) => {
+ setTimeout(() => {
+ const freqs = [523, 659, 784, 1047, 1319];
+ playTone({ freq: freqs[i], type: 'sine', vol: 0.22 - i*0.02, dur: 0.3, attack: 0.01 });
+ }, ms);
+ });
+ },
+ lose() {
+ playTone({ freq: 220, type: 'sawtooth', vol: 0.1, dur: 0.35, attack: 0.01 });
+ setTimeout(() => playTone({ freq: 165, type: 'sawtooth', vol: 0.08, dur: 0.4 }), 180);
+ },
+ push() {
+ playTone({ freq: 440, type: 'triangle', vol: 0.1, dur: 0.2 });
+ setTimeout(() => playTone({ freq: 440, type: 'triangle', vol: 0.07, dur: 0.2 }), 220);
+ },
+ bust() {
+ playTone({ freq: 300, type: 'sawtooth', vol: 0.12, dur: 0.15 });
+ setTimeout(() => playTone({ freq: 200, type: 'sawtooth', vol: 0.1, dur: 0.25 }), 100);
+ },
+ click() {
+ playTone({ freq: 600, type: 'sine', vol: 0.06, dur: 0.06 });
+ }
+};
+
+// ═══════════════════════════════════════════════════════
+// CARD DECK
+// ═══════════════════════════════════════════════════════
+const SUITS = ['♠', '♥', '♦', '♣'];
+const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
+const RED_SUITS = new Set(['♥', '♦']);
+
+function buildDeck() {
+ const deck = [];
+ for (const suit of SUITS)
+ for (const rank of RANKS)
+ deck.push({ rank, suit });
+ return deck;
+}
+
+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 cardValue(rank) {
+ if (rank === 'A') return 11;
+ if (['J', 'Q', 'K'].includes(rank)) return 10;
+ return parseInt(rank);
+}
+
+function handTotal(cards) {
+ let total = 0, aces = 0;
+ for (const c of cards) {
+ total += cardValue(c.rank);
+ if (c.rank === 'A') aces++;
+ }
+ while (total > 21 && aces > 0) { total -= 10; aces--; }
+ return total;
+}
+
+function isSoft(cards) {
+ // returns true if hand has an ace counted as 11
+ let total = 0, aces = 0;
+ for (const c of cards) {
+ total += cardValue(c.rank);
+ if (c.rank === 'A') aces++;
+ }
+ let softAce = aces > 0 && total <= 21;
+ while (total > 21 && aces > 0) { total -= 10; aces--; softAce = false; }
+ return softAce;
+}
+
+function isBlackjack(cards) {
+ return cards.length === 2 && handTotal(cards) === 21;
+}
+
+// ═══════════════════════════════════════════════════════
+// GAME STATE
+// ═══════════════════════════════════════════════════════
+let deck = [];
+let playerCards = [];
+let dealerCards = [];
+let phase = 'betting'; // betting | player | dealer | done
+let currentBet = 0;
+let bankroll = 1000;
+let bestWin = 0;
+let streak = 0;
+let doubled = false;
+
+// Persist
+function saveState() {
+ localStorage.setItem('bj90210_bankroll', bankroll);
+ localStorage.setItem('bj90210_bestWin', bestWin);
+}
+function loadState() {
+ bankroll = parseInt(localStorage.getItem('bj90210_bankroll') || '1000');
+ bestWin = parseInt(localStorage.getItem('bj90210_bestWin') || '0');
+ if (isNaN(bankroll) || bankroll < 1) bankroll = 1000;
+ if (isNaN(bestWin)) bestWin = 0;
+}
+
+// ═══════════════════════════════════════════════════════
+// DOM REFS
+// ═══════════════════════════════════════════════════════
+const $ = id => document.getElementById(id);
+const dh = $('dealerHand');
+const ph = $('playerHand');
+const dealerScore = $('dealerScore');
+const playerScore = $('playerScore');
+const statusMsg = $('statusMsg');
+const betArea = $('betArea');
+const actionRow = $('actionRow');
+const dealRow = $('dealRow');
+const betDisplay = $('betDisplay');
+const bankrollDisp = $('bankrollDisplay');
+const currentBetDisp= $('currentBetDisplay');
+const bestWinDisp = $('bestWinDisplay');
+const streakBadge = $('streakBadge');
+const streakVal = $('streakVal');
+const resultOverlay = $('resultOverlay');
+const resultCard = $('resultCard');
+const hitBtn = $('hitBtn');
+const standBtn = $('standBtn');
+const doubleBtn = $('doubleBtn');
+const dealBtn = $('dealBtn');
+const nextHandBtn = $('nextHandBtn');
+const muteBtn = $('muteBtn');
+
+// ═══════════════════════════════════════════════════════
+// UI HELPERS
+// ═══════════════════════════════════════════════════════
+function fmt(n) { return '$' + n.toLocaleString(); }
+
+function updateBankrollUI() {
+ bankrollDisp.textContent = fmt(bankroll);
+ currentBetDisp.textContent = currentBet > 0 ? fmt(currentBet) : '—';
+ bestWinDisp.textContent = fmt(bestWin);
+ betDisplay.textContent = currentBet > 0 ? `Bet: ${fmt(currentBet)}` : '';
+ dealBtn.disabled = currentBet <= 0 || currentBet > bankroll;
+}
+
+function updateStreak() {
+ if (streak >= 2) {
+ streakBadge.style.display = 'inline-flex';
+ streakVal.textContent = streak;
+ } else {
+ streakBadge.style.display = 'none';
+ }
+}
+
+function setStatus(msg) { statusMsg.textContent = msg; }
+
+function showBettingUI() {
+ betArea.style.display = 'flex';
+ actionRow.style.display = 'none';
+ dealRow.style.display = 'flex';
+ setChipsEnabled(true);
+}
+
+function showActionUI(enableDouble) {
+ betArea.style.display = 'none';
+ actionRow.style.display = 'flex';
+ dealRow.style.display = 'none';
+ hitBtn.disabled = false;
+ standBtn.disabled = false;
+ doubleBtn.disabled = !enableDouble;
+}
+
+function hideAllActions() {
+ betArea.style.display = 'none';
+ actionRow.style.display = 'none';
+ dealRow.style.display = 'none';
+}
+
+function setChipsEnabled(on) {
+ document.querySelectorAll('.chip[data-val]').forEach(c => c.disabled = !on);
+ $('clearBetBtn').disabled = !on;
+}
+
+// ═══════════════════════════════════════════════════════
+// CARD RENDERING
+// ═══════════════════════════════════════════════════════
+function makeCardEl(card, faceDown = false, animated = true) {
+ const el = document.createElement('div');
+ el.classList.add('card');
+ if (!animated) el.style.animation = 'none';
+
+ if (faceDown) {
+ el.classList.add('face-down');
+ el.dataset.card = JSON.stringify(card);
+ return el;
+ }
+
+ el.classList.add('face-up');
+ if (RED_SUITS.has(card.suit)) el.classList.add('red');
+
+ el.innerHTML = `
+ <div class="card-corner">
+ <div class="card-rank">${card.rank}</div>
+ <div class="card-suit">${card.suit}</div>
+ </div>
+ <div class="card-center">${card.suit}</div>
+ <div class="card-corner bottom">
+ <div class="card-rank">${card.rank}</div>
+ <div class="card-suit">${card.suit}</div>
+ </div>
+ `;
+ return el;
+}
+
+function revealHoleCard() {
+ const holeEl = dh.querySelector('.face-down');
+ if (!holeEl) return;
+ const card = JSON.parse(holeEl.dataset.card);
+ const newEl = makeCardEl(card, false, false);
+ newEl.classList.add('flip-reveal');
+ dh.replaceChild(newEl, holeEl);
+}
+
+function addCardToHand(hand, handEl, card, faceDown = false, delay = 0) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ hand.push(card);
+ const el = makeCardEl(card, faceDown);
+ handEl.appendChild(el);
+ sounds.deal();
+ resolve();
+ }, delay);
+ });
+}
+
+function updateScores(showDealer = false) {
+ const ps = handTotal(playerCards);
+ playerScore.style.visibility = 'visible';
+ playerScore.textContent = ps;
+ playerScore.className = 'score-badge';
+ if (ps > 21) playerScore.classList.add('bust');
+ else if (ps === 21) playerScore.classList.add('blackjack');
+ else if (ps >= 18) playerScore.classList.add('good');
+
+ if (showDealer) {
+ const ds = handTotal(dealerCards);
+ dealerScore.style.visibility = 'visible';
+ dealerScore.textContent = ds;
+ dealerScore.className = 'score-badge';
+ if (ds > 21) dealerScore.classList.add('bust');
+ else if (ds === 21) dealerScore.classList.add('blackjack');
+ else if (ds >= 17) dealerScore.classList.add('good');
+ } else {
+ // Show only first dealer card value
+ if (dealerCards.length > 0) {
+ dealerScore.style.visibility = 'visible';
+ dealerScore.textContent = cardValue(dealerCards[0].rank);
+ dealerScore.className = 'score-badge';
+ }
+ }
+}
+
+// ═══════════════════════════════════════════════════════
+// DECK MANAGEMENT
+// ═══════════════════════════════════════════════════════
+function ensureDeck() {
+ if (deck.length < 15) {
+ deck = shuffle(buildDeck());
+ setStatus('Shuffling deck…');
+ }
+}
+
+function draw() {
+ ensureDeck();
+ return deck.pop();
+}
+
+// ═══════════════════════════════════════════════════════
+// RESULT / OVERLAY
+// ═══════════════════════════════════════════════════════
+function showResult(headline, sub, amount, type) {
+ const h = $('resultHeadline');
+ const s = $('resultSub');
+ const a = $('resultAmount');
+ h.textContent = headline;
+ h.className = 'result-headline ' + (type === 'lose' ? 'lose' : type === 'push' ? 'push' : '');
+ s.textContent = sub;
+ a.textContent = amount;
+ a.className = 'result-amount ' + type;
+ resultCard.style.animation = 'none';
+ resultCard.offsetHeight; // reflow
+ resultCard.style.animation = '';
+ resultOverlay.classList.add('show');
+}
+
+function hideResult() {
+ resultOverlay.classList.remove('show');
+}
+
+// ═══════════════════════════════════════════════════════
+// DEAL
+// ═══════════════════════════════════════════════════════
+async function deal() {
+ if (currentBet <= 0 || currentBet > bankroll || phase !== 'betting') return;
+ sounds.click();
+
+ // Deduct bet from bankroll immediately
+ bankroll -= currentBet;
+ doubled = false;
+ saveState();
+
+ // Clear hands
+ playerCards = [];
+ dealerCards = [];
+ dh.innerHTML = '';
+ ph.innerHTML = '';
+ dealerScore.style.visibility = 'hidden';
+ playerScore.style.visibility = 'hidden';
+ hideResult();
+ hideAllActions();
+ phase = 'dealing';
+ setStatus('Dealing…');
+
+ // Deal: player, dealer, player, dealer(face-down)
+ await addCardToHand(playerCards, ph, draw(), false, 0);
+ await addCardToHand(dealerCards, dh, draw(), false, 200);
+ await addCardToHand(playerCards, ph, draw(), false, 400);
+ await addCardToHand(dealerCards, dh, draw(), true, 600);
+
+ updateScores(false);
+ updateBankrollUI();
+
+ // Check player blackjack
+ if (isBlackjack(playerCards)) {
+ await new Promise(r => setTimeout(r, 200));
+ ph.classList.add('bj-shimmer');
+ revealHoleCard();
+ updateScores(true);
+
+ if (isBlackjack(dealerCards)) {
+ // Both blackjack = push
+ await new Promise(r => setTimeout(r, 600));
+ resolvePush('Both have Blackjack');
+ } else {
+ // Player BJ wins 3:2
+ const win = Math.floor(currentBet * 1.5);
+ await new Promise(r => setTimeout(r, 600));
+ resolveWin(win, 'Blackjack!', true);
+ }
+ return;
+ }
+
+ // Check dealer blackjack (peek)
+ if (isBlackjack(dealerCards)) {
+ await new Promise(r => setTimeout(r, 200));
+ revealHoleCard();
+ updateScores(true);
+ await new Promise(r => setTimeout(r, 600));
+ resolveLose('Dealer has Blackjack');
+ return;
+ }
+
+ // Normal play
+ phase = 'player';
+ const canDouble = bankroll >= currentBet; // has enough for another bet
+ showActionUI(canDouble);
+ setStatus('Your turn');
+}
+
+// ═══════════════════════════════════════════════════════
+// PLAYER ACTIONS
+// ═══════════════════════════════════════════════════════
+function hit() {
+ if (phase !== 'player') return;
+ sounds.click();
+ const card = draw();
+ playerCards.push(card);
+ ph.appendChild(makeCardEl(card));
+ sounds.deal();
+
+ updateScores(false);
+ doubleBtn.disabled = true; // can't double after hitting
+
+ const total = handTotal(playerCards);
+ if (total > 21) {
+ playerScore.classList.add('bust');
+ $('feltTable').classList.add('bust-flash');
+ setTimeout(() => $('feltTable').classList.remove('bust-flash'), 1000);
+ hideAllActions();
+ setStatus('Bust!');
+ sounds.bust();
+ setTimeout(() => resolveLose('Busted'), 800);
+ } else if (total === 21) {
+ stand();
+ }
+}
+
+function stand() {
+ if (phase !== 'player') return;
+ sounds.click();
+ phase = 'dealer';
+ hideAllActions();
+ dealerTurn();
+}
+
+function doubleDown() {
+ if (phase !== 'player' || bankroll < currentBet) return;
+ sounds.click();
+ sounds.chip();
+ bankroll -= currentBet;
+ currentBet *= 2;
+ doubled = true;
+ saveState();
+ updateBankrollUI();
+ setStatus('Doubled down!');
+
+ const card = draw();
+ playerCards.push(card);
+ ph.appendChild(makeCardEl(card));
+ sounds.deal();
+ updateScores(false);
+
+ const total = handTotal(playerCards);
+ if (total > 21) {
+ playerScore.classList.add('bust');
+ $('feltTable').classList.add('bust-flash');
+ setTimeout(() => $('feltTable').classList.remove('bust-flash'), 1000);
+ setStatus('Bust!');
+ sounds.bust();
+ setTimeout(() => resolveLose('Busted on Double'), 800);
+ } else {
+ phase = 'dealer';
+ setTimeout(() => dealerTurn(), 400);
+ }
+}
+
+// ═══════════════════════════════════════════════════════
+// DEALER TURN
+// ═══════════════════════════════════════════════════════
+async function dealerTurn() {
+ phase = 'dealer';
+ revealHoleCard();
+ await new Promise(r => setTimeout(r, 350));
+ updateScores(true);
+
+ let dealerTotal = handTotal(dealerCards);
+ setStatus(`Dealer: ${dealerTotal}`);
+
+ while (dealerTotal < 17 || (dealerTotal === 17 && isSoft(dealerCards))) {
+ await new Promise(r => setTimeout(r, 600));
+ const card = draw();
+ dealerCards.push(card);
+ dh.appendChild(makeCardEl(card));
+ sounds.deal();
+ dealerTotal = handTotal(dealerCards);
+ updateScores(true);
+ setStatus(`Dealer: ${dealerTotal}`);
+ }
+
+ await new Promise(r => setTimeout(r, 400));
+
+ const playerTotal = handTotal(playerCards);
+
+ if (dealerTotal > 21) {
+ resolveWin(currentBet, 'Dealer Busted', false);
+ } else if (dealerTotal > playerTotal) {
+ resolveLose(`Dealer ${dealerTotal} vs Your ${playerTotal}`);
+ } else if (playerTotal > dealerTotal) {
+ resolveWin(currentBet, `Your ${playerTotal} vs Dealer ${dealerTotal}`, false);
+ } else {
+ resolvePush(`${playerTotal} — Push`);
+ }
+}
+
+// ═══════════════════════════════════════════════════════
+// RESOLUTION
+// ═══════════════════════════════════════════════════════
+function resolveWin(winAmount, sub, bj = false) {
+ bankroll += currentBet + winAmount; // return bet + profit
+ if (winAmount > bestWin) { bestWin = winAmount; }
+ streak++;
+ saveState();
+ updateBankrollUI();
+ updateStreak();
+ phase = 'done';
+ setStatus('');
+ if (bj) sounds.blackjack(); else sounds.win();
+ showResult('WIN', sub, `+${fmt(winAmount)}`, 'win');
+}
+
+function resolveLose(sub) {
+ // bet already deducted
+ streak = 0;
+ saveState();
+ updateBankrollUI();
+ updateStreak();
+ phase = 'done';
+ setStatus('');
+ sounds.lose();
+ showResult('LOSE', sub, `−${fmt(currentBet)}`, 'lose');
+ checkBankrupt();
+}
+
+function resolvePush(sub) {
+ bankroll += currentBet; // return bet
+ saveState();
+ updateBankrollUI();
+ phase = 'done';
+ setStatus('');
+ sounds.push();
+ showResult('PUSH', sub, fmt(currentBet) + ' returned', 'push');
+}
+
+function checkBankrupt() {
+ if (bankroll <= 0) {
+ setTimeout(() => {
+ setStatus('Bankrupt — resetting to $1,000');
+ bankroll = 1000;
+ saveState();
+ updateBankrollUI();
+ }, 2000);
+ }
+}
+
+// ═══════════════════════════════════════════════════════
+// NEXT HAND / RESET
+// ═══════════════════════════════════════════════════════
+function nextHand() {
+ hideResult();
+ phase = 'betting';
+ currentBet = 0;
+ playerCards = [];
+ dealerCards = [];
+ dh.innerHTML = '';
+ ph.innerHTML = '';
+ dealerScore.style.visibility = 'hidden';
+ playerScore.style.visibility = 'hidden';
+ setStatus('Place your bet to begin');
+ showBettingUI();
+ updateBankrollUI();
+}
+
+function resetBankroll() {
+ if (confirm) { /* no native confirm — just reset silently after a hand */ }
+ bankroll = 1000;
+ bestWin = 0;
+ streak = 0;
+ saveState();
+ nextHand();
+ setStatus('Bankroll reset to $1,000');
+ updateStreak();
+}
+
+// ═══════════════════════════════════════════════════════
+// KEYBOARD
+// ═══════════════════════════════════════════════════════
+document.addEventListener('keydown', e => {
+ if (e.repeat) return;
+ const k = e.key.toLowerCase();
+ if (phase === 'player') {
+ if (k === 'h' || k === 'arrowup') { e.preventDefault(); hit(); }
+ if (k === 's' || k === 'arrowdown') { e.preventDefault(); stand(); }
+ if (k === 'd') { e.preventDefault(); if (!doubleBtn.disabled) doubleDown(); }
+ }
+ if (phase === 'betting') {
+ if (k === 'enter' || k === ' ') { e.preventDefault(); if (!dealBtn.disabled) deal(); }
+ }
+ if (phase === 'done') {
+ if (k === 'enter' || k === ' ' || k === 'n') { e.preventDefault(); nextHand(); }
+ }
+ if (k === 'm') { muted = !muted; muteBtn.textContent = muted ? '🔇' : '🔊'; }
+});
+
+// ═══════════════════════════════════════════════════════
+// EVENT LISTENERS
+// ═══════════════════════════════════════════════════════
+document.querySelectorAll('.chip[data-val]').forEach(chip => {
+ chip.addEventListener('click', () => {
+ if (phase !== 'betting') return;
+ const val = parseInt(chip.dataset.val);
+ const newBet = currentBet + val;
+ if (newBet > bankroll) return; // can't bet more than bankroll
+ currentBet = newBet;
+ sounds.chip();
+ updateBankrollUI();
+ });
+});
+
+$('clearBetBtn').addEventListener('click', () => {
+ if (phase !== 'betting') return;
+ currentBet = 0;
+ sounds.click();
+ updateBankrollUI();
+});
+
+dealBtn.addEventListener('click', deal);
+hitBtn.addEventListener('click', hit);
+standBtn.addEventListener('click', stand);
+doubleBtn.addEventListener('click', doubleDown);
+nextHandBtn.addEventListener('click', nextHand);
+$('resetBtn').addEventListener('click', resetBankroll);
+muteBtn.addEventListener('click', () => {
+ muted = !muted;
+ muteBtn.textContent = muted ? '🔇' : '🔊';
+});
+
+// ═══════════════════════════════════════════════════════
+// INIT
+// ═══════════════════════════════════════════════════════
+loadState();
+deck = shuffle(buildDeck());
+updateBankrollUI();
+updateStreak();
+showBettingUI();
+setStatus('Place your bet to begin');
+</script>
+</body>
+</html>
← 7527c24 Agent-driven pivot: DW Color Story Studio (frontend-develope
·
back to Model Arena
·
auto-save: 2026-07-25T09:05:11 (2 files) — staging-90210/902 52073ae →