Phase 4. Hold'em, and it's the only table in the casino that is a session rather than a game: you buy in, play as many hands as you like, and leave with what's in front of you. So the live row spans hands and chips cross the border exactly twice. Everything in between is inside the engine. The bots move inside ApplyMove, as UNO's do, which is what keeps poker off a socket: shove all-in and the flop, turn, river, showdown and payout all come back in one response, as a script the felt plays back. The CFR policy the plan called "the single highest-value asset in either repo" was never read. Not once, in the whole life of the game: the trainer wrote its info-set keys under IP/OOP and the runtime looked them up under BTN/SB/BB, so every lookup missed and fell silently through to a pot-odds heuristic. Nothing looked broken, because a policy miss is not an error. And it was the wrong policy anyway — ten big blinds deep, trained on a tree where a call always ends the street, which is not poker. So the trainer is rewritten to play the real engine through the real reducer, at every stack depth the table deals, and the trainer and the table now build the key with the same function so they cannot drift apart again. A test fails if the bots stop finding themselves in it. Three money bugs, and the tests earned their keep. Chip conservation across a hundred sessions caught an uncalled bet that minted chips. A var-init ordering trap meant every card was identical, every showdown tied and every bot believed it held exactly 50% equity. And the browser caught the rake being silently zero — the tier said 5 meaning percent, the casino handed it 0.05 meaning a fraction, and integer division took the house's cut down to nothing. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
213 lines
12 KiB
HTML
213 lines
12 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>
|
||
|
||
<!-- 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>
|
||
|
||
<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}}
|