Blackjack has a split. It was the last rule missing from a game that has been live for a week, and it is the only move in blackjack that takes chips out of your stack *after* the cards are out — which is most of what there is to get wrong about it. So the state stops pretending. State.Player is gone; there is a slice of Hands, each with its own cards, its own bet, its own outcome and its own payout, and an Active index the player works left to right. Settle runs per hand and rakes per hand: netting them against each other first would mean a player who won one and lost one paid no rake at all, which is not a rake, it's a discount for splitting. The web layer takes the second bet before the move and hands it straight back if the engine refuses — the same shape double already used, except double was staking st.Bet, the whole table's stake, which was the same number as the hand's until today and is now emphatically not. DoubleCost/SplitCost are the active hand's, and the felt would have found this by charging you 300 to double the third hand of a split. The rules that cost money if you guess them: split aces get one card each and no say (a pair of aces is otherwise the best hand in the game, forever), 21 on a split hand is twenty-one and not a natural (it does not pay 3:2 — the test that pins this is the most expensive one in the file), same rank rather than same value (a king and a queen are not a pair), four hands maximum, double after split allowed, and if every hand busts the dealer does not turn over. A live hand outlives a deploy, so State.UnmarshalJSON still reads the old blobs: "player" with no "hands" becomes one hand holding the whole stake. Without it, a player mid-hand at restart is a player whose cards vanished — which is not a decode error, and would not have looked like one. On the felt a hand is now a box with its own spot, and a split is a card lifting out of one hand into a new one with a second stack of chips flying after it from your pile. Verified in a browser against a real pair: chips 4738 -> 4638 on the split, two hands played out, one push and one loss, "Down on the deal. -100", 4738 back. Three hands stack without collision at 390px. Settled hands come back to full brightness — dimming means "not your turn", and when the deal is over they are the thing you are reading. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
154 lines
7.7 KiB
HTML
154 lines
7.7 KiB
HTML
{{define "title"}}Blackjack · {{.Room.Name}}{{end}}
|
|
|
|
{{define "main"}}
|
|
<div class="space-y-6" data-blackjack>
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
|
</svg>
|
|
<span class="sr-only">Back to the casino</span>
|
|
</a>
|
|
<h1 class="font-display text-3xl font-bold">Blackjack</h1>
|
|
</div>
|
|
<p class="text-sm text-[color:var(--ink)]/50">Six decks · pays 3:2 · dealer hits soft 17</p>
|
|
</div>
|
|
|
|
{{template "_chipbar" .}}
|
|
|
|
<!-- The felt. The two things on it that aren't cards are the shoe the cards
|
|
come out of and the rack the money comes out of — a chip on this table is
|
|
always travelling between one of those and the spot in front of you. -->
|
|
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
|
|
|
<div class="pete-rack" data-at="shoe" data-house aria-hidden="true">
|
|
<span data-chip="500" style="--stack: 5"></span>
|
|
<span data-chip="100" style="--stack: 7"></span>
|
|
<span data-chip="25" style="--stack: 4"></span>
|
|
<span data-chip="5" style="--stack: 6"></span>
|
|
</div>
|
|
|
|
<!-- The shoe every card flies out of. -->
|
|
<div class="pete-shoe" aria-hidden="true"></div>
|
|
|
|
<div class="relative space-y-8">
|
|
|
|
<!-- Dealer -->
|
|
<div>
|
|
<div class="mb-2 flex items-center gap-2">
|
|
<span data-dealer-label class="text-xs font-bold uppercase tracking-wider text-white/60">Dealer</span>
|
|
<span data-dealer-total class="hidden rounded-full bg-black/25 px-2.5 py-0.5 text-xs font-bold tabular-nums text-white"></span>
|
|
</div>
|
|
<div data-dealer class="pete-hand" aria-live="polite"></div>
|
|
</div>
|
|
|
|
<!-- What just happened. -->
|
|
<div class="flex min-h-[2.75rem] items-center justify-center">
|
|
<!-- The pill is white, so its text is ink — not var(--ink), which is the
|
|
room's cream and would be white on white. -->
|
|
<p data-verdict class="hidden rounded-full bg-white/95 px-5 py-2 font-display text-lg font-bold text-[#2b2118] shadow-pete"></p>
|
|
</div>
|
|
|
|
<!-- Player: the bet sits in front of the cards it's riding on.
|
|
|
|
The hands themselves are built by blackjack.js, because how many there
|
|
are is a thing that changes mid-hand: you split, and one hand with one
|
|
bet on it becomes two hands with a bet each. The template below is the
|
|
shape of one of them; the row starts with exactly one, which is also
|
|
the spot you build your bet on before any cards exist. -->
|
|
<div>
|
|
<div class="mb-2 flex items-center gap-2">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-white/60">You</span>
|
|
</div>
|
|
<div class="bj-hands" data-hands data-count="1" aria-live="polite"></div>
|
|
</div>
|
|
|
|
<template data-hand-template>
|
|
<div class="bj-hand" data-hand data-live="1">
|
|
<div class="pete-spot" data-spot>
|
|
<span class="pete-spot-label">Bet</span>
|
|
<div class="pete-stack" data-stack></div>
|
|
<span data-spot-total class="pete-spot-total hidden"></span>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="mb-1 flex h-5 items-center gap-1.5">
|
|
<span data-total class="hidden rounded-full bg-black/25 px-2.5 py-0.5 text-xs font-bold tabular-nums text-white"></span>
|
|
<span data-hand-outcome class="hidden rounded-full bg-white/90 px-2 py-0.5 text-[0.65rem] font-bold uppercase tracking-wide text-[#2b2118]"></span>
|
|
</div>
|
|
<div data-cards class="pete-hand"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Betting: shown between hands. -->
|
|
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
|
<div>
|
|
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your bet</div>
|
|
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
{{range .Denominations}}
|
|
<button type="button" data-chip="{{.}}" aria-label="Bet {{.}} more"
|
|
class="pete-chip pete-disc grid h-12 w-12 place-items-center font-display text-sm font-bold text-white">
|
|
<span>{{.}}</span>
|
|
</button>
|
|
{{end}}
|
|
<button type="button" data-bet-clear
|
|
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
|
</div>
|
|
|
|
<button type="button" data-deal
|
|
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
|
Deal
|
|
</button>
|
|
</div>
|
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
|
</section>
|
|
|
|
<!-- Playing: shown while a hand is live. -->
|
|
<section data-actions class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
|
<div class="flex flex-wrap items-center justify-center gap-3">
|
|
<button type="button" data-move="hit"
|
|
class="rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
|
Hit
|
|
</button>
|
|
<button type="button" data-move="stand"
|
|
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
|
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
|
Stand
|
|
</button>
|
|
<button type="button" data-move="double"
|
|
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
|
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
|
Double
|
|
</button>
|
|
<button type="button" data-move="split"
|
|
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
|
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
|
Split
|
|
</button>
|
|
</div>
|
|
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
|
<kbd>h</kbd> hit · <kbd>s</kbd> stand · <kbd>d</kbd> double · <kbd>p</kbd> split
|
|
</p>
|
|
</section>
|
|
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script src="/static/js/casino-fx.js" defer></script>
|
|
<script src="/static/js/casino-cards.js" defer></script>
|
|
<script src="/static/js/games.js" defer></script>
|
|
<script src="/static/js/blackjack.js" defer></script>
|
|
{{end}}
|