Solitaire, Vegas rules — the only shape solitaire has ever had as a gambling game. You don't win or lose the deal: the stake buys the deck outright, and every card you get home to a foundation pays a fifty-second of the tier's multiple back. Cash the board whenever you like and keep what you've banked, so a board that has gone dead is a decision rather than a wall. No undo: the stake is spent the moment the deck is bought, and an undo would be a way to walk a losing board backwards until it wins. Three deals, and the two dials are the whole difficulty of Klondike. Patient draws one with unlimited passes and pays 1.4x, so it takes 38 cards home to get square. Vegas draws three, three times round, 2.2x, square at 24. Cutthroat draws three and gives you one pass, 3.4x, square at 16 — most of those boards never clear, and you're ahead long before they would. internal/games/klondike is the same pure reducer as the other two, and Pays() is one function for the same reason hangman's is. Two fuzzers hold the deck together: no sequence of moves can lose or duplicate a card, and the board stays well-formed. They earned their keep immediately — the first thing they caught was a recycle that reversed the waste. It flips as a block, so the card drawn first comes out first, and reversing it would have dealt a different game on every pass and quietly broken the seed in the audit log. The browser never sees the stock or a face-down card, which here is most of the deck rather than blackjack's one hole card: a column sends how many cards are under it, never which. The table re-renders and animates the difference. Blackjack plays back a script because a hand only ever grows at one end; solitaire moves runs from anywhere to anywhere and an auto-finish moves eleven cards at once, so a script of "append this card there" would be a second engine over here and it would be the one that's wrong. Instead the board on screen is always exactly the board the server says exists, and each card is played from where it just was to where it now is. The events supply only what a diff can't: where a newly-revealed card came from, and what the board is worth. The rules are mirrored in JS on purpose, and only to light up the columns a held card can go to. Being shown where a card goes is the game teaching you; being told no after you commit is the game scolding you. The server still decides, and a disagreement snaps the board back to what it says. Two things came out into the open rather than being copied, which is the rule this room runs on: casino-cards.js (the deck — faces, pips, the flip) and PeteFX.spot() (the pile of chips and the number under it, which now owns the rule that the number is a readout of the pile). Blackjack uses both. Not yet driven in a browser.
134 lines
6.5 KiB
HTML
134 lines
6.5 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-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. -->
|
|
<div>
|
|
<div class="mb-2 flex items-center gap-2">
|
|
<span class="text-xs font-bold uppercase tracking-wider text-white/60">You</span>
|
|
<span data-player-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 class="flex items-center gap-5">
|
|
<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 data-player class="pete-hand flex-1" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</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>
|
|
</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
|
|
</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}}
|