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.
169 lines
8.2 KiB
HTML
169 lines
8.2 KiB
HTML
{{define "title"}}Solitaire · {{.Room.Name}}{{end}}
|
||
|
||
{{define "main"}}
|
||
<div class="space-y-6" data-solitaire>
|
||
|
||
<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">Solitaire</h1>
|
||
</div>
|
||
<p class="text-sm text-[color:var(--ink)]/50">You buy the deck. Every card you get home buys some of it back.</p>
|
||
</div>
|
||
|
||
{{template "_chipbar" .}}
|
||
|
||
<!-- The felt. The board takes the room; the money lives in a rail down the
|
||
right of it, where the house rack can't collide with the foundations. -->
|
||
<section class="pete-felt pete-solitaire relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
||
|
||
<!-- The board. -->
|
||
<div class="min-w-0 space-y-4 sm:space-y-6">
|
||
|
||
<!-- The stock and the waste on the left, the foundations on the right,
|
||
exactly where a real layout puts them. -->
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="flex items-start gap-2 sm:gap-3">
|
||
<button type="button" data-stock class="pete-slot pete-stock" aria-label="Turn over the stock">
|
||
<span data-stock-count class="pete-slot-count">0</span>
|
||
<span data-stock-recycle class="pete-slot-recycle hidden" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path><polyline points="21 3 21 8 16 8"></polyline>
|
||
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path><polyline points="3 21 3 16 8 16"></polyline>
|
||
</svg>
|
||
</span>
|
||
</button>
|
||
<div data-waste class="pete-waste" aria-label="The waste"></div>
|
||
</div>
|
||
|
||
<div data-foundations class="flex items-start gap-1.5 sm:gap-2" aria-label="The foundations"></div>
|
||
</div>
|
||
|
||
<!-- The seven columns. -->
|
||
<div data-tableau class="pete-tableau" aria-label="The tableau"></div>
|
||
|
||
<!-- What just happened. -->
|
||
<div class="flex min-h-[2.75rem] items-center">
|
||
<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>
|
||
</div>
|
||
|
||
<!-- The rail: the house's rack, what you've banked, and the meter that
|
||
reads it. Everything the money does happens between these two. -->
|
||
<aside class="pete-rail">
|
||
<div class="pete-rack" data-at="rail" 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>
|
||
|
||
<div class="pete-spot" data-spot>
|
||
<span class="pete-spot-label">Banked</span>
|
||
<div class="pete-stack" data-stack></div>
|
||
<span data-spot-total class="pete-spot-total hidden"></span>
|
||
</div>
|
||
|
||
<div class="pete-meter w-full justify-center" data-meter>
|
||
<span class="pete-meter-label">Home</span>
|
||
<span data-home class="pete-meter-value">0<span class="text-white/40">/{{.FullDeck}}</span></span>
|
||
</div>
|
||
|
||
<p class="text-center text-xs leading-relaxed text-white/55">
|
||
<span data-per-card class="font-bold text-white/85">—</span> a card<br>
|
||
<span data-break-even></span>
|
||
</p>
|
||
</aside>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Playing: shown while a board is live. -->
|
||
<section data-playing 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 gap-3">
|
||
<button type="button" data-auto
|
||
class="rounded-full bg-[color:var(--ink)]/5 px-5 py-2.5 font-display font-bold border-2 border-[color:var(--ink)]/10
|
||
hover:bg-[color:var(--ink)]/10 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Send home ⤴
|
||
</button>
|
||
|
||
<p class="text-xs text-[color:var(--ink)]/45">
|
||
Click a card, then where it goes. Double-click sends it home.
|
||
</p>
|
||
|
||
<button type="button" data-cash
|
||
class="ml-auto rounded-full bg-[color:var(--accent)] px-6 py-2.5 font-display font-bold text-white shadow-pete
|
||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Cash the board · <span data-cash-amount class="tabular-nums">0</span>
|
||
</button>
|
||
</div>
|
||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
||
</section>
|
||
|
||
<!-- Buying a deck: shown between games. -->
|
||
<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="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Which deal?</div>
|
||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||
{{range .Deals}}
|
||
<button type="button" data-tier="{{.Slug}}"
|
||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||
<div class="flex items-baseline justify-between gap-2">
|
||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{printf "%.1f" .Base}}×</span>
|
||
</div>
|
||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
||
Square with the house at {{.BreakEven}} cards home.
|
||
</p>
|
||
</button>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="mt-5 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">The deck costs</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="Put {{.}} more down"
|
||
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-start
|
||
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">
|
||
Buy the deck
|
||
</button>
|
||
</div>
|
||
<p class="mt-3 text-xs text-[color:var(--ink)]/40">
|
||
The stake buys the deck outright, so it doesn't come back. What comes back is
|
||
whatever you get home, a fifty-second of the multiple at a time. Stop whenever
|
||
you like and keep it. There's no undo.
|
||
</p>
|
||
<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>
|
||
|
||
</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/solitaire.js" defer></script>
|
||
{{end}}
|