Phase C frontend: the hold'em felt runs on the shared-table runtime. - holdem.js reads view.your_seat instead of assuming seat zero — every "you" test (layout, your cards, the burst on a pot you win, the verdict) is keyed on it now, so a joiner at seat 2 sees their own hand at the bottom. - Leaving is its own endpoint, and a bust closes a solo table; play() animates a session-ending hand (the last showdown) before the felt clears. - A live table: one EventSource per seated player. The server pushes a nudge on every table change and a chat line as it is said; a nudge refetches the player's own redacted view (a hole card must never ride a frame that fans to the table), and a frame that lands mid-animation is held until the script finishes. - Chat on the felt (a _chat panel, messages only) and a lobby that lists tables with a seat going spare. Two-cookie dev rig (reala + bob), with the turn clock and reaper live under it. Browser-confirmed for solo: sit renders your seat and the rail, a hand deals and conserves to the chip (bought in 100, 100 in front), chat sends. The two-browser multiplayer pass (join, live sync between windows, shared-table conservation) is still owed before this deploys. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
235 lines
13 KiB
HTML
235 lines
13 KiB
HTML
{{define "title"}}Hold'em · {{.Room.Name}}{{end}}
|
||
|
||
{{define "main"}}
|
||
<div class="space-y-6" data-holdem>
|
||
|
||
<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">Texas Hold'em</h1>
|
||
</div>
|
||
<p class="text-sm text-[color:var(--ink)]/50">Buy in, play as long as you like, leave with what's in front of you</p>
|
||
</div>
|
||
|
||
{{template "_chipbar" .}}
|
||
|
||
<!-- The felt. Seats along the top, the board and the pot in the middle, you at
|
||
the bottom. Every seat has its own bet spot, and every chip on this table is
|
||
travelling between one of those and the pot — which is the whole difference
|
||
between poker and every other game in the room, where the chips only ever
|
||
move between you and the house. -->
|
||
<section class="pete-felt pete-poker 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-5 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
||
|
||
<div class="min-w-0">
|
||
<div data-seats class="pete-poker-seats" aria-label="The other players"></div>
|
||
|
||
<div class="pete-poker-middle">
|
||
<div data-board class="pete-poker-board" aria-label="The board"></div>
|
||
|
||
<div class="pete-poker-pot">
|
||
<!-- The chips get a box of their own. .pete-stack is absolutely
|
||
positioned over whatever contains it, so a pile sharing a box with
|
||
the number under it paints straight over the number. -->
|
||
<div class="pete-poker-pot-pile">
|
||
<div class="pete-stack" data-pot-stack></div>
|
||
</div>
|
||
<span class="pete-poker-pot-label">Pot</span>
|
||
<span data-pot-total class="pete-poker-pot-total tabular-nums">0</span>
|
||
<span data-side class="pete-poker-side hidden"></span>
|
||
</div>
|
||
|
||
<div class="flex min-h-[2.75rem] items-center justify-center">
|
||
<p data-verdict class="pete-poker-verdict hidden" aria-live="polite"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pete-poker-you flex flex-col items-center" data-you></div>
|
||
</div>
|
||
|
||
<!-- The rail. This table has no corner free for the house's rack — the seats
|
||
run along the top and your hand is under the board — so it takes
|
||
solitaire's rail and sits off the felt entirely. -->
|
||
<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="text-center">
|
||
<div class="pete-meter" data-meter>
|
||
<span class="pete-meter-label">Blinds</span>
|
||
<span data-blinds class="pete-meter-value">—</span>
|
||
</div>
|
||
<p data-table-name class="mt-1.5 text-xs font-bold uppercase tracking-wider text-white/40"></p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Acting: shown when the hand is yours to play. -->
|
||
<section data-acting 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-move="fold"
|
||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-6 py-3 font-display text-lg font-bold shadow-pete
|
||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Fold
|
||
</button>
|
||
<button type="button" data-move="check"
|
||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-6 py-3 font-display text-lg font-bold shadow-pete
|
||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Check
|
||
</button>
|
||
<button type="button" data-move="call"
|
||
class="rounded-full bg-[color:var(--accent)] px-6 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">
|
||
Call <span data-call-amount class="tabular-nums"></span>
|
||
</button>
|
||
</div>
|
||
|
||
<div data-raise-row class="mt-4 flex flex-wrap items-center gap-3 border-t-2 border-[color:var(--ink)]/5 pt-4">
|
||
<div class="pete-raise">
|
||
<input type="range" data-raise-slider aria-label="How much to raise to">
|
||
<span data-raise-to class="pete-raise-to">0</span>
|
||
</div>
|
||
<div class="flex flex-wrap items-center gap-1.5">
|
||
<button type="button" data-raise-preset="0.5" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">½ pot</button>
|
||
<button type="button" data-raise-preset="1" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">Pot</button>
|
||
<button type="button" data-raise-preset="max" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">Max</button>
|
||
</div>
|
||
<button type="button" data-move="raise"
|
||
class="ml-auto rounded-full bg-[color:var(--accent)] px-7 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">
|
||
<span data-raise-verb>Raise to</span> <span data-raise-label class="tabular-nums"></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>
|
||
|
||
<!-- Between hands: deal the next one, put more chips out, or get up. -->
|
||
<section data-between 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">
|
||
<div class="min-w-0">
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">In front of you</div>
|
||
<div class="font-display text-3xl font-bold tabular-nums" data-table-stack>0</div>
|
||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/40">
|
||
Bought in for <span data-bought-in class="tabular-nums">0</span> · the house has taken <span data-session-rake class="tabular-nums">0</span> in rake
|
||
</p>
|
||
</div>
|
||
|
||
<div class="ml-auto flex flex-wrap items-center gap-2">
|
||
<button type="button" data-topup
|
||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-5 py-3 font-display text-base font-bold shadow-pete
|
||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Top up
|
||
</button>
|
||
<button type="button" data-leave
|
||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-5 py-3 font-display text-base font-bold shadow-pete
|
||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||
Get up
|
||
</button>
|
||
<button type="button" data-deal
|
||
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">
|
||
Next hand
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p data-between-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
||
</section>
|
||
|
||
<!-- The rail talk. Chat lives at the felt and does not leave it for Matrix;
|
||
every line is stamped with the hand it was said during. Shown while you're
|
||
seated. -->
|
||
<section data-chat class="hidden rounded-3xl bg-[color:var(--card)] p-4 shadow-pete border-2 border-[color:var(--ink)]/10">
|
||
<div data-chat-log class="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1 text-sm" aria-live="polite" aria-label="Table chat"></div>
|
||
<form data-chat-form class="mt-3 flex items-center gap-2">
|
||
<input data-chat-input type="text" maxlength="240" autocomplete="off" placeholder="Say something to the table…"
|
||
class="flex-1 min-w-0 rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--bg)] px-4 py-2 text-sm focus:outline-none focus:border-[color:var(--accent)]">
|
||
<button type="submit"
|
||
class="rounded-full bg-[color:var(--accent)] px-5 py-2 font-display text-sm font-bold text-white shadow-pete hover:brightness-105 active:translate-y-px transition">
|
||
Say
|
||
</button>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- Sitting down: shown when you aren't at a table. -->
|
||
<section data-sitting 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">What are you playing for?</div>
|
||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||
{{range .Stakes}}
|
||
<button type="button" data-tier="{{.Slug}}"
|
||
data-min="{{.MinBuy}}" data-max="{{.MaxBuy}}" data-bb="{{.BB}}"
|
||
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)]">{{.SB}}/{{.BB}}</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">
|
||
Buy in {{.MinBuy}}–{{.MaxBuy}}
|
||
</p>
|
||
</button>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="mt-5 grid gap-5 sm:grid-cols-2">
|
||
<div>
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">How many of them?</div>
|
||
<div class="mt-2 flex flex-wrap gap-1.5" data-bots>
|
||
<button type="button" data-bot-count="1" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">1</button>
|
||
<button type="button" data-bot-count="2" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">2</button>
|
||
<button type="button" data-bot-count="3" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">3</button>
|
||
<button type="button" data-bot-count="4" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">4</button>
|
||
<button type="button" data-bot-count="5" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">5</button>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-[color:var(--ink)]/40" data-bots-note></p>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Buying in for</div>
|
||
<div class="mt-2 flex items-center gap-3">
|
||
<input type="range" data-buyin-slider class="flex-1 min-w-0" aria-label="How much to buy in for">
|
||
<span data-buyin class="font-display text-2xl font-bold tabular-nums">0</span>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-[color:var(--ink)]/40" data-buyin-note></p>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="button" data-sit
|
||
class="mt-5 w-full 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">
|
||
Sit down
|
||
</button>
|
||
|
||
<!-- Or pull up a chair at a table someone else has already opened. Bots keep
|
||
every open table full, so there is always a seat to take. -->
|
||
<div data-lobby-wrap class="mt-6 hidden">
|
||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Or join a table in progress</div>
|
||
<div data-lobby class="mt-2 grid gap-2"></div>
|
||
</div>
|
||
|
||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
||
The bots are trained, not scripted. The house takes {{.RakePct}}% of a pot that sees a flop, capped at three big blinds, and nothing at all from a hand that doesn't.
|
||
</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/holdem.js" defer></script>
|
||
{{end}}
|