From e6c1bd3b54b5b8b6ad7735a8fbe22e8e0734a345 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:08:59 -0700 Subject: [PATCH] games: the poker table opens, and the bots go back to school MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/holdem-train/main.go | 89 +++ go.mod | 1 + go.sum | 7 + internal/games/holdem/betting.go | 279 +++++++++ internal/games/holdem/cfr.go | 416 +++++++++++++ internal/games/holdem/equity.go | 134 +++++ internal/games/holdem/eval.go | 263 +++++++++ internal/games/holdem/holdem.go | 846 +++++++++++++++++++++++++++ internal/games/holdem/holdem_test.go | 641 ++++++++++++++++++++ internal/games/holdem/policy.gob | Bin 0 -> 203953 bytes internal/games/holdem/train.go | 428 ++++++++++++++ internal/web/games_holdem.go | 347 +++++++++++ internal/web/games_holdem_test.go | 239 ++++++++ internal/web/games_pages.go | 23 +- internal/web/games_play.go | 12 + internal/web/games_render_test.go | 1 + internal/web/server.go | 2 +- internal/web/static/css/input.css | 254 ++++++++ internal/web/static/css/output.css | 2 +- internal/web/static/js/holdem.js | 648 ++++++++++++++++++++ internal/web/templates/games.html | 16 + internal/web/templates/holdem.html | 212 +++++++ pete_games_plan.md | 132 ++++- 23 files changed, 4969 insertions(+), 23 deletions(-) create mode 100644 cmd/holdem-train/main.go create mode 100644 internal/games/holdem/betting.go create mode 100644 internal/games/holdem/cfr.go create mode 100644 internal/games/holdem/equity.go create mode 100644 internal/games/holdem/eval.go create mode 100644 internal/games/holdem/holdem.go create mode 100644 internal/games/holdem/holdem_test.go create mode 100644 internal/games/holdem/policy.gob create mode 100644 internal/games/holdem/train.go create mode 100644 internal/web/games_holdem.go create mode 100644 internal/web/games_holdem_test.go create mode 100644 internal/web/static/js/holdem.js create mode 100644 internal/web/templates/holdem.html diff --git a/cmd/holdem-train/main.go b/cmd/holdem-train/main.go new file mode 100644 index 0000000..8ff77ac --- /dev/null +++ b/cmd/holdem-train/main.go @@ -0,0 +1,89 @@ +// Command holdem-train trains the casino's poker bots and writes the policy the +// table embeds. +// +// go run ./cmd/holdem-train -iterations 5000000 -out internal/games/holdem/policy.gob +// +// It is not part of Pete. It runs when the engine's rules change, takes half an +// hour, and produces a file. Nothing at runtime imports it. +// +// The bots are trained heads-up, at every stack depth the table deals — that +// range is the point, because poker at twenty big blinds and poker at a hundred +// are different games and a bot that only knows one of them folds into the other. +// A six-handed table then reuses the same policy, which is an approximation, and +// a documented one. +package main + +import ( + "flag" + "fmt" + "log" + "os" + "runtime" + "time" + + "pete/internal/games/holdem" +) + +func main() { + iterations := flag.Int("iterations", 5_000_000, "hands to train on") + workers := flag.Int("workers", runtime.NumCPU(), "parallel workers") + out := flag.String("out", "internal/games/holdem/policy.gob", "where to write the policy") + stakes := flag.String("stakes", "low", "which table's blinds to train at") + minBB := flag.Int64("min-bb", 20, "shallowest stack, in big blinds") + maxBB := flag.Int64("max-bb", 100, "deepest stack, in big blinds") + seed := flag.Uint64("seed", 20260714, "seed, so a run can be repeated") + flag.Parse() + + tier, err := holdem.TierBySlug(*stakes) + if err != nil { + log.Fatalf("no such table %q", *stakes) + } + + fmt.Printf("training %s hands on %d workers — %s blinds, %d–%d BB deep\n", + commas(*iterations), *workers, tier.Name, *minBB, *maxBB) + + started := time.Now() + last := started + policy := holdem.Train(*iterations, *workers, tier, *minBB, *maxBB, *seed, func(done int) { + if time.Since(last) < 20*time.Second { + return + } + last = time.Now() + frac := float64(done) / float64(*iterations) + if frac <= 0 { + return + } + left := time.Duration(float64(time.Since(started)) * (1 - frac) / frac) + fmt.Printf(" %s / %s hands (%.0f%%), about %s to go\n", + commas(done), commas(*iterations), frac*100, left.Round(time.Second)) + }) + + f, err := os.Create(*out) + if err != nil { + log.Fatal(err) + } + defer f.Close() + if err := holdem.Save(f, policy); err != nil { + log.Fatal(err) + } + + info, _ := f.Stat() + fmt.Printf("\ndone in %s: %s nodes, %.1f MB → %s\n", + time.Since(started).Round(time.Second), commas(policy.Meta.Nodes), + float64(info.Size())/(1<<20), *out) +} + +func commas(n int) string { + s := fmt.Sprint(n) + if len(s) <= 3 { + return s + } + var out []byte + for i, c := range []byte(s) { + if i > 0 && (len(s)-i)%3 == 0 { + out = append(out, ',') + } + out = append(out, c) + } + return string(out) +} diff --git a/go.mod b/go.mod index 9c2151b..4166aa2 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/chehsunliu/poker v0.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect diff --git a/go.sum b/go.sum index a287e45..72b8029 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1 github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/chehsunliu/poker v0.1.0 h1:OeB4O+QROhA/DiXUhBBlkgbzCx0ZVWMpWgKNu+PX9vI= +github.com/chehsunliu/poker v0.1.0/go.mod h1:V6K4yyDbafp0k6lUnYbwoTS/KsHSB1EWiJdEk54uB1w= github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I= github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,6 +33,7 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/loganjspears/joker v0.0.0-20180219043703-3f2f69a75914/go.mod h1:76SAnflG7ZFhgtnaVCpP6A5Z1S/VMFzRBN7KGm5j4oc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= @@ -48,6 +51,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/notnil/joker v0.0.0-20180219043703-3f2f69a75914/go.mod h1:L0Sdr2nYdktjerdXpIn9wOCn+GebPs/nCL2qH6RTGa0= github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -58,6 +62,7 @@ github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -157,6 +162,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ= diff --git a/internal/games/holdem/betting.go b/internal/games/holdem/betting.go new file mode 100644 index 0000000..7390941 --- /dev/null +++ b/internal/games/holdem/betting.go @@ -0,0 +1,279 @@ +package holdem + +import "sort" + +// The betting rules. These are the fiddly ones — min-raise, short all-ins that +// don't reopen the action, side pots — and they came over from gogobee, where +// they had no tests at all. They have some now. + +// blinds posts the small and the big. Heads-up is the exception every poker +// implementation gets wrong once: with two players the button *is* the small +// blind and acts first before the flop, and last after it. +func (s *State) blinds(evs *[]Event) (bb int) { + var sb int + if s.dealt() == 2 { + sb, bb = s.Button, s.nextIn(s.Button) + } else { + sb = s.nextIn(s.Button) + bb = s.nextIn(sb) + } + + s.post(sb, s.Tier.SB, "small", evs) + s.post(bb, s.Tier.BB, "big", evs) + + s.Bet = s.Tier.BB + s.MinRaise = s.Tier.BB + s.Aggressor = bb // the big blind has the option to raise their own blind + return bb +} + +// post puts a blind up. A player too short to cover it is all-in for what they +// have, which is legal and is why the amount is clamped rather than refused. +func (s *State) post(seat int, amount int64, which string, evs *[]Event) { + p := &s.Seats[seat] + if amount > p.Stack { + amount = p.Stack + } + p.Stack -= amount + p.Bet = amount + p.Total = amount + if p.Stack == 0 { + p.State = AllIn + } + *evs = append(*evs, Event{Kind: "blind", Seat: seat, Amount: amount, Text: which}) +} + +// firstPreFlop is under the gun: the seat after the big blind, or the button +// itself when the table is heads-up. +// +// The button only gets it if the button can still act. A short stack can be +// all-in on its own blind — post a small blind of 1 with 1 chip left and you are +// in the hand with no chips and no say — and handing the action to a seat that +// cannot act wedges the table. +func (s *State) firstPreFlop(bb int) int { + if s.dealt() == 2 && s.Seats[s.Button].State == Active { + return s.Button + } + if s.dealt() == 2 { + return s.nextCanAct(s.Button) + } + return s.nextCanAct(bb) +} + +// firstPostFlop is the first seat left of the button, on every street after the +// flop. The button acts last from here on, which is the whole point of it. +func (s *State) firstPostFlop() int { return s.nextCanAct(s.Button) } + +// ---- the five things a seat can do ---------------------------------------- + +func (s *State) fold(seat int, evs *[]Event) { + p := &s.Seats[seat] + p.State = Folded + p.Acted = true + s.History += "f" + *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "fold"}) +} + +func (s *State) check(seat int, evs *[]Event) error { + p := &s.Seats[seat] + if p.Bet < s.Bet { + return ErrCantCheck + } + p.Acted = true + s.History += "c" + *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "check"}) + return nil +} + +func (s *State) call(seat int, evs *[]Event) error { + p := &s.Seats[seat] + owed := s.Bet - p.Bet + if owed <= 0 { + return ErrNothingToCall + } + if owed > p.Stack { + owed = p.Stack // a call for less than the bet is a call all-in + } + + p.Stack -= owed + p.Bet += owed + p.Total += owed + p.Acted = true + + text := "call" + if p.Stack == 0 { + p.State = AllIn + text = "allin" + s.History += "a" + } else { + s.History += "c" + } + *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: owed, Total: p.Bet}) + return nil +} + +// raise raises *to* a total, not *by* an amount. Every poker interface in the +// world means the total, and a browser that means the other thing bets wrong. +func (s *State) raise(seat int, to int64, evs *[]Event) error { + p := &s.Seats[seat] + most := p.Bet + p.Stack + + if to > most { + return ErrTooBig + } + if to < s.Bet+s.MinRaise && to < most { + return ErrTooSmall // only a shove may be smaller than a legal raise + } + + added := to - p.Bet + over := to - s.Bet + + p.Stack -= added + p.Bet = to + p.Total += added + p.Acted = true + + if over > 0 { + s.MinRaise = over + } + s.Bet = to + s.Aggressor = seat + + text := "raise" + if p.Stack == 0 { + p.State = AllIn + text = "allin" + s.History += "a" + } else { + // The policy was trained against a tree with two raise sizes in it, so the + // history it reads has to say which one this was: R for a pot-sized raise + // or bigger, r for anything smaller. + if pot := s.inPlay(); pot > 0 && float64(over) >= float64(pot)*0.75 { + s.History += "R" + } else { + s.History += "r" + } + } + *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: added, Total: to}) + return nil +} + +// allin pushes the lot. +// +// A short all-in does not reopen the betting. If a player shoves for less than +// a full raise over the current bet, players who have already acted may call it +// but may not raise again — otherwise a tiny stack could be used to reopen the +// action for a partner, which is the oldest collusion trick there is. +func (s *State) allin(seat int, evs *[]Event) error { + p := &s.Seats[seat] + if p.Stack <= 0 { + return ErrNoChips + } + added := p.Stack + to := p.Bet + added + + p.Stack = 0 + p.Bet = to + p.Total += added + p.State = AllIn + p.Acted = true + + if to > s.Bet { + if over := to - s.Bet; over >= s.MinRaise { + s.MinRaise = over + s.Aggressor = seat + } + s.Bet = to + } + + s.History += "a" + *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "allin", Amount: added, Total: to}) + return nil +} + +// ---- when is a street over ------------------------------------------------ + +// streetDone reports whether the betting round is finished, given the seat the +// action would pass to next. +// +// The "has acted" check is the load-bearing half. The big blind has money in +// front of them without having chosen to put it there, so a round where +// everybody merely limps in has all bets matched while the blind has never had +// a say. Without this, they never get their option. +func (s *State) streetDone(next int) bool { + if s.canActCount() == 0 { + return true + } + for i := range s.Seats { + p := &s.Seats[i] + if p.State != Active { + continue + } + if p.Bet != s.Bet || !p.Acted { + return false + } + } + // The last aggressor being all-in means the action can't get back to them: + // everyone left has matched the bet above, so there is nothing more to do. + if s.Seats[s.Aggressor].State == AllIn { + return true + } + return next == s.Aggressor +} + +// ---- side pots ------------------------------------------------------------- + +// sidePots slices the pot into layers, one per distinct all-in level. A player +// can only win the part of the pot they could have lost, so each layer is +// contested by exactly the players who paid into it. +// +// Folded players' chips stay in the pot — they paid for the right to fold — but +// they are eligible for nothing. +func (s *State) sidePots() { + s.collect() + + var levels []int64 + for i := range s.Seats { + p := &s.Seats[i] + if p.State == Folded || p.State == Out || p.Total == 0 { + continue + } + levels = append(levels, p.Total) + } + if len(levels) == 0 { + return + } + sort.Slice(levels, func(i, j int) bool { return levels[i] < levels[j] }) + + var pots []Pot + var prev int64 + for _, level := range levels { + if level <= prev { + continue + } + var amount int64 + var eligible []int + for i := range s.Seats { + p := &s.Seats[i] + paid := p.Total - prev + if paid > level-prev { + paid = level - prev + } + if paid > 0 { + amount += paid // folded money counts toward the pot... + } + if p.State != Folded && p.State != Out && p.Total >= level { + eligible = append(eligible, i) // ...but wins no part of it + } + } + if amount > 0 { + pots = append(pots, Pot{Amount: amount, Eligible: eligible}) + } + prev = level + } + + if len(pots) > 0 { + s.Side = pots + s.Pot = 0 + } +} diff --git a/internal/games/holdem/cfr.go b/internal/games/holdem/cfr.go new file mode 100644 index 0000000..471d574 --- /dev/null +++ b/internal/games/holdem/cfr.go @@ -0,0 +1,416 @@ +package holdem + +import ( + _ "embed" + "fmt" + "log/slog" + "math/rand/v2" + "sync" + "sync/atomic" + + "pete/internal/games/cards" +) + +// The bots' brain. +// +// gogobee ran counterfactual regret minimisation against this game for a very +// long time, and policy.gob is what it converged on: a table from "the situation +// I am in" to "how often I fold, call, raise small, raise big, or shove". It is +// the single highest-value thing in either repository, and none of it is +// re-derived here — this file is the *runtime*, the trainer stayed behind. +// +// A situation is squeezed down to six things, and this is the whole reason the +// table fits in memory: the street, whether the bot is in position, which of +// twelve equity buckets its hand falls in, which of five stack-to-pot buckets, +// whether the board is dry, wet or paired, and the last six actions. Two hands +// that hash to the same key get the same strategy, and that is the approximation +// the whole thing is built on. +// +// The key has to be *exactly* the key the trainer wrote, character for +// character. Change the bucket edges, the position label or the history encoding +// and every lookup misses — silently, because a miss is not an error, it is a +// fall back to the pot-odds rule below. The bots would get quietly, unaccountably +// worse. So: don't touch these numbers. + +//go:embed policy.gob +var policyGob []byte + +// The five things the trainer let a bot consider. +const ( + actFold = iota + actCallCheck + actRaiseHalf + actRaisePot + actAllIn + numActions +) + +// policyTable maps an info-set key to how often to take each action. +type policyTable map[string][numActions]float64 + +var ( + policyOnce sync.Once + policy policyTable +) + +// loadPolicy decodes the embedded table, once, on the first hand anybody plays. +// +// Not in an init(): it is megabytes of gob, and Pete is a news server that mostly +// never deals a card. Every test in the repo and every cold start would pay for +// it. The first player to sit down pays for it instead, and only they do. +func loadPolicy() policyTable { + policyOnce.Do(func() { + t, err := loadTrained(policyGob) + if err != nil { + // The bots still play — on pot odds — rather than the table 500ing. + slog.Error("holdem: cannot decode CFR policy, bots fall back to pot odds", "err", err) + policy = policyTable{} + return + } + policy = t.Strategy + slog.Info("holdem: CFR policy loaded", "nodes", len(policy), + "iterations", t.Meta.Iterations, "stakes", t.Meta.Stakes, "depths", t.Meta.Depths) + }) + return policy +} + +// ---- the info-set key ------------------------------------------------------ +// +// Every function below is a load-bearing copy of the trainer's. See the note at +// the top of the file before changing a number in any of them. + +// equityBucket puts a hand's strength in one of twelve boxes, from trash to +// monster. +func equityBucket(eq float64) int { + switch { + case eq < 0.08: + return 0 + case eq < 0.17: + return 1 + case eq < 0.25: + return 2 + case eq < 0.33: + return 3 + case eq < 0.42: + return 4 + case eq < 0.50: + return 5 + case eq < 0.58: + return 6 + case eq < 0.67: + return 7 + case eq < 0.75: + return 8 + case eq < 0.83: + return 9 + case eq < 0.92: + return 10 + default: + return 11 + } +} + +// sprBucket is the stack-to-pot ratio: how much room is left to play. Under 1 is +// a pot that is already committed; over 12 is a pot you can still fold out of. +func sprBucket(spr float64) int { + switch { + case spr < 1: + return 0 + case spr < 3: + return 1 + case spr < 6: + return 2 + case spr < 12: + return 3 + default: + return 4 + } +} + +// Board textures, which is what makes the same hand a bet or a check. +const ( + boardDry = 0 + boardWet = 1 + boardPaired = 2 +) + +// boardTexture classifies the community cards. A paired board is one somebody +// might have trips on; a wet one has a flush or straight coming. +// It is on the trainer's hot path — millions of calls — so it counts into arrays +// rather than maps. A map here cost more than the poker did. +func boardTexture(board []cards.Card) int { + if len(board) < 3 { + return boardDry + } + + var ranks [14]int8 + var suits [4]int8 + var vals [5]int + for i, c := range board { + ranks[c.Rank]++ + suits[c.Suit]++ + vals[i] = int(c.Rank) + } + for _, n := range ranks { + if n >= 2 { + return boardPaired + } + } + for _, n := range suits { + if n >= 3 { + return boardWet + } + } + + // Three cards inside a five-rank window is a straight waiting to happen. + v := vals[:len(board)] + for i := 1; i < len(v); i++ { + for j := i; j > 0 && v[j] < v[j-1]; j-- { + v[j], v[j-1] = v[j-1], v[j] + } + } + for i := 0; i+2 < len(v); i++ { + if v[i+2]-v[i] <= 4 { + return boardWet + } + } + return boardDry +} + +// infoSet builds the string the policy is keyed on. The format is the trainer's. +// +// Position is IP or OOP — in position or out of it — and *nothing else*. This is +// the one thing gogobee got wrong, and it got it wrong invisibly for as long as +// the game has existed: the trainer packed a single "am I last to act" bit and +// wrote its keys as IP/OOP, while the runtime looked them up with the table +// labels a player would recognise (BTN, SB, BB, UTG…). Not one key ever matched. +// Every bot in every hand of hold'em gogobee ever dealt fell through to the +// pot-odds rule, and the five million training iterations sitting in policy.gob +// were never once read. +// +// Nothing about that looks broken from the outside. A missing key is not an +// error, it's a fallback — the bots played, they just played a heuristic. This is +// why the hit rate is now a test. +func infoSet(street Street, inPosition bool, eq, spr, texture int, history string) string { + pos := "OOP" + if inPosition { + pos = "IP" + } + return fmt.Sprintf("%d|%s|%d|%d|%d|%s", street, pos, eq, spr, texture, history) +} + +// recent keeps the last six actions, which is all the trainer's key had room for. +func recent(h string) string { + if len(h) > 6 { + return h[len(h)-6:] + } + return h +} + +// ---- where a seat stands --------------------------------------------------- + +// mcIters is how many runouts a bot samples to judge its hand at the table. +const mcIters = 1000 + +// spot is everything the policy knows about a seat's situation, and it is the +// one function that builds it. +// +// The trainer calls this too. That is the point of it: the key the policy is +// written under and the key it is read under come out of the same code, so they +// cannot quietly stop matching — which is exactly what went wrong the first time +// and went unnoticed for the life of the game. +func (s State) spot(seat, iters int, rng *rand.Rand) (string, Equity) { + eq := s.equityFor(seat, iters, rng) + return s.spotKey(seat, eq), eq +} + +// equityFor measures how good a seat's hand is right now. +// +// It depends only on the cards — the hand, the board, how many opponents — and +// not on a single thing that happened in the betting. Which is why the trainer +// can measure it once per street and reuse it down every branch it explores, and +// why doing that is most of the difference between a run that takes half an hour +// and one that takes four. +func (s State) equityFor(seat, iters int, rng *rand.Rand) Equity { + opponents := s.liveCount() - 1 + if opponents < 1 { + opponents = 1 + } + // Preflop heads-up is a lookup, not a simulation: there are only 169 hands + // that differ, they have been measured to death, and a sampled answer would + // only add noise to a bucket boundary. + if s.Street == PreFlop && opponents == 1 { + return preflopEquity(s.Seats[seat].Hole) + } + return equityOf(s.Seats[seat].Hole, s.Community, opponents, iters, rng) +} + +// spotKey builds the key from an equity already measured. +func (s State) spotKey(seat int, eq Equity) string { + pot := s.inPlay() + spr := 0.0 + if pot > 0 { + spr = float64(s.Seats[seat].Stack) / float64(pot) + } + return infoSet(s.Street, s.InPosition(seat), equityBucket(eq.Strength()), + sprBucket(spr), boardTexture(s.Community), recent(s.History)) +} + +// ---- choosing -------------------------------------------------------------- + +// Hits and Misses count how often a bot finds itself in the trained policy. +// +// They exist because the way this can break is silently. A key the policy has +// never seen is not an error — the bot shrugs and plays pot odds — so a policy +// that has stopped matching the game looks exactly like a policy that is working. +// gogobee's never matched once, for the whole life of the game, and nobody could +// have known by watching it play. Now a test reads these and fails. +var Hits, Misses atomic.Int64 + +// botActs plays one bot's turn: work out where it stands, look up what it does +// there, throw out anything illegal, and roll for it. +func (s *State) botActs(seat int, evs *[]Event, rng *rand.Rand) { + key, eq := s.spot(seat, mcIters, rng) + + probs, ok := loadPolicy()[key] + if ok { + Hits.Add(1) + } else { + Misses.Add(1) + probs = potOdds(eq, s, seat) + } + probs = legal(probs, s, seat) + + move := s.moveFor(pick(probs, rng), seat) + if err := s.act(seat, move, evs); err != nil { + // A bot cannot be allowed to wedge the table by choosing something the rules + // then refuse: it checks if it can and folds if it can't, and the mismatch + // is loud, because it means legal() and the betting rules disagree. + slog.Error("holdem: bot chose an illegal move", "seat", seat, "move", move.Kind, "err", err) + if s.Owed(seat) > 0 { + s.fold(seat, evs) + } else { + _ = s.check(seat, evs) + } + } +} + +// potOdds is what a bot does when the trained table has never seen this spot: it +// works out whether the price it is being offered beats its chance of winning, +// and mixes in enough aggression not to be a calling station. +func potOdds(eq Equity, s *State, seat int) [numActions]float64 { + p := &s.Seats[seat] + strength := eq.Strength() + owed := s.Bet - p.Bet + pot := s.inPlay() + + price := 0.0 + if owed > 0 && pot+owed > 0 { + price = float64(owed) / float64(pot+owed) + } + + var probs [numActions]float64 + switch { + case strength > 0.8: + probs[actRaisePot], probs[actAllIn], probs[actCallCheck] = 0.6, 0.2, 0.2 + case strength > 0.6: + probs[actRaiseHalf], probs[actCallCheck], probs[actFold] = 0.4, 0.5, 0.1 + case owed > 0 && strength > price: + probs[actCallCheck], probs[actRaiseHalf], probs[actFold] = 0.7, 0.2, 0.1 + case owed > 0: + probs[actFold], probs[actCallCheck] = 0.7, 0.3 + default: + probs[actCallCheck], probs[actRaiseHalf], probs[actFold] = 0.6, 0.3, 0.1 + } + return probs +} + +// mask is what a seat may actually do here. The trainer explores exactly this +// set, so it never learns a strategy the table would turn down. +func (s State) mask(seat int) (m [numActions]bool) { + owed := s.Bet - s.Seats[seat].Bet + + // Folding a hand you could see for free is a bug, not a strategy. + m[actFold] = owed > 0 + m[actCallCheck] = true + + // A raise needs chips behind the call, and somebody left to bet into. + raise := s.Seats[seat].Stack > owed && s.canBet() + m[actRaiseHalf], m[actRaisePot], m[actAllIn] = raise, raise, raise + return m +} + +// legal zeroes out what the seat cannot do and renormalises what's left. +func legal(probs [numActions]float64, s *State, seat int) [numActions]float64 { + m := s.mask(seat) + var total float64 + for i := range probs { + if !m[i] { + probs[i] = 0 + } + total += probs[i] + } + if total <= 0 { + var only [numActions]float64 + only[actCallCheck] = 1 + return only + } + for i := range probs { + probs[i] /= total + } + return probs +} + +// pick rolls against the distribution. +func pick(probs [numActions]float64, rng *rand.Rand) int { + r := rng.Float64() + sum := 0.0 + for i, p := range probs { + sum += p + if r < sum { + return i + } + } + return actCallCheck +} + +// moveFor turns an abstract action — "raise half the pot" — into a legal move at +// the actual size the table is at. A raise that would cost the bot everything it +// has is a shove, which is the same decision made honestly. +func (s *State) moveFor(action, seat int) Move { + p := &s.Seats[seat] + owed := s.Bet - p.Bet + pot := s.inPlay() + most := p.Bet + p.Stack + + sized := func(by int64) Move { + if by < s.MinRaise { + by = s.MinRaise + } + to := s.Bet + by + if to >= most { + return Move{Kind: Shove} + } + return Move{Kind: Raise, To: to} + } + + switch action { + case actFold: + if owed <= 0 { + return Move{Kind: Check} // never fold for free + } + return Move{Kind: Fold} + case actCallCheck: + if owed > 0 { + return Move{Kind: Call} + } + return Move{Kind: Check} + case actRaiseHalf: + return sized(pot / 2) + case actRaisePot: + return sized(pot) + case actAllIn: + return Move{Kind: Shove} + } + return Move{Kind: Check} +} diff --git a/internal/games/holdem/equity.go b/internal/games/holdem/equity.go new file mode 100644 index 0000000..76190e8 --- /dev/null +++ b/internal/games/holdem/equity.go @@ -0,0 +1,134 @@ +package holdem + +import ( + "math/rand/v2" + + "github.com/chehsunliu/poker" + + "pete/internal/games/cards" +) + +// How good is this hand, really? +// +// A bot's decision starts here: deal the cards it cannot see, a thousand times, +// and count how often it wins. That number — plus the board's texture, the +// stack-to-pot ratio and the action so far — is the key it looks its trained +// strategy up under. +// +// Monte Carlo rather than exhaustive because exhaustive is 2.1 million river +// runouts against one opponent and rather more against five, and a thousand +// samples puts the estimate inside a percentage point or so. The bot does not +// need the fourth decimal place; it needs to know whether it is ahead. + +// Equity is the fraction of runouts a hand wins, ties and loses against the +// given number of unknown opponents. +type Equity struct { + Win float64 + Tie float64 + Loss float64 +} + +// Strength collapses a result into the one number the policy is keyed on: a tie +// is worth half a win, because that is what half a pot is. +func (e Equity) Strength() float64 { return e.Win + e.Tie*0.5 } + +// deck52 is the evaluator's whole deck, built once. +var deck52 = func() []poker.Card { + d := make([]poker.Card, 0, 52) + for s := cards.Spades; s <= cards.Clubs; s++ { + for r := cards.Ace; r <= cards.King; r++ { + d = append(d, pokerOf[s][r]) + } + } + return d +}() + +// equityOf runs the simulation. The RNG is threaded like everything else here, +// so a bot's decision replays from the session's seed along with the deal. +func equityOf(hole [2]cards.Card, board []cards.Card, opponents, iterations int, rng *rand.Rand) Equity { + if opponents < 1 { + opponents = 1 + } + + h0, h1 := toPoker(hole[0]), toPoker(hole[1]) + + // Seven cards, checked by hand. A map here would be the most expensive thing + // in the trainer, which calls this function millions of times. + var known [7]poker.Card + n := 2 + known[0], known[1] = h0, h1 + pb := make([]poker.Card, len(board)) + for i, c := range board { + pb[i] = toPoker(c) + known[n] = pb[i] + n++ + } + + rest := make([]poker.Card, 0, 52) + for _, c := range deck52 { + seen := false + for _, k := range known[:n] { + if k == c { + seen = true + break + } + } + if !seen { + rest = append(rest, c) + } + } + + need := opponents*2 + (5 - len(pb)) + if need > len(rest) { + return Equity{Tie: 1} + } + + hero := make([]poker.Card, 7) + hero[0], hero[1] = h0, h1 + villain := make([]poker.Card, 7) + full := make([]poker.Card, 5) + + var wins, ties int + for i := 0; i < iterations; i++ { + // A partial Fisher-Yates: only the cards actually needed get shuffled into + // place, which is the difference between this being cheap and being the + // slowest thing in the request. + for j := 0; j < need; j++ { + k := j + rng.IntN(len(rest)-j) + rest[j], rest[k] = rest[k], rest[j] + } + + copy(full, pb) + at := opponents * 2 + for b := len(pb); b < 5; b++ { + full[b] = rest[at] + at++ + } + + copy(hero[2:], full) + mine := poker.Evaluate(hero) + + best := int32(7463) // one worse than the worst real hand + for o := 0; o < opponents; o++ { + villain[0], villain[1] = rest[o*2], rest[o*2+1] + copy(villain[2:], full) + if r := poker.Evaluate(villain); r < best { + best = r + } + } + + switch { + case mine < best: + wins++ + case mine == best: + ties++ + } + } + + total := float64(iterations) + return Equity{ + Win: float64(wins) / total, + Tie: float64(ties) / total, + Loss: float64(iterations-wins-ties) / total, + } +} diff --git a/internal/games/holdem/eval.go b/internal/games/holdem/eval.go new file mode 100644 index 0000000..3ef0e56 --- /dev/null +++ b/internal/games/holdem/eval.go @@ -0,0 +1,263 @@ +package holdem + +import ( + "math" + "sort" + "strings" + + "github.com/chehsunliu/poker" + + "pete/internal/games/cards" +) + +// The bridge to the evaluator. +// +// The engine deals Pete's own cards.Card — the same one blackjack, solitaire and +// the felt already speak — and converts at the door. Hand strength is the one +// thing in this package that is genuinely hard to get right (7-card best-of-5, +// 7,462 distinct hands), so it is not homegrown: github.com/chehsunliu/poker is +// a lookup table and it is correct. +// +// The conversion is a table built once. Doing it per evaluation would matter: +// a bot's equity estimate is a thousand seven-card evaluations, and it makes +// several of those per hand. + +var ( + pokerRanks = [14]string{"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"} + pokerSuits = [4]string{"s", "h", "d", "c"} // cards.Spades, Hearts, Diamonds, Clubs + + // A var initializer, not an init(). Go builds package-level variables before + // it runs init functions, so anything else in this package that is itself a + // var built out of this table — equity.go's deck52 is — would otherwise be + // built out of an empty one. It was, briefly: every card came out identical, + // every showdown tied, and every bot believed it held exactly 50% equity. + pokerOf = func() (t [4][14]poker.Card) { + for s := cards.Spades; s <= cards.Clubs; s++ { + for r := cards.Ace; r <= cards.King; r++ { + t[s][r] = poker.NewCard(pokerRanks[r] + pokerSuits[s]) + } + } + return t + }() +) + +// toPoker converts one card for the evaluator. +func toPoker(c cards.Card) poker.Card { return pokerOf[c.Suit][c.Rank] } + +func toPokerAll(cs []cards.Card) []poker.Card { + out := make([]poker.Card, len(cs)) + for i, c := range cs { + out[i] = toPoker(c) + } + return out +} + +// rankOf evaluates a seat's best five from its hole cards and the board. Lower +// is better — 1 is a royal flush — which is the evaluator's convention and not +// worth inverting, since nothing outside this file ever sees the number. +func rankOf(hole [2]cards.Card, board []cards.Card) (int32, string) { + seven := make([]poker.Card, 0, 7) + seven = append(seven, toPoker(hole[0]), toPoker(hole[1])) + seven = append(seven, toPokerAll(board)...) + r := poker.Evaluate(seven) + return r, strings.ToLower(poker.RankString(r)) +} + +// ---- showdown ------------------------------------------------------------- + +type ranked struct { + seat int + rank int32 + desc string +} + +// showdown turns the cards over, splits the pots, and pays. Every player still +// in the hand shows, in the order the felt should turn them over: best hand +// first, so the winner is the first card face the player sees. +func (s *State) showdown(evs *[]Event) { + s.collect() + s.Street = Showdown + + // Say so. The last street's bets are still sitting in front of the seats that + // made them, as far as the felt knows, and nothing else in the script is going + // to tell it they have been swept in. + *evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()}) + + var live []ranked + for i := range s.Seats { + p := &s.Seats[i] + if p.State == Folded || p.State == Out { + continue + } + r, desc := rankOf(p.Hole, s.Community) + live = append(live, ranked{seat: i, rank: r, desc: desc}) + } + sort.Slice(live, func(i, j int) bool { return live[i].rank < live[j].rank }) + + for _, e := range live { + *evs = append(*evs, Event{ + Kind: "show", Seat: e.seat, + Cards: []cards.Card{s.Seats[e.seat].Hole[0], s.Seats[e.seat].Hole[1]}, + Text: e.desc, + }) + } + + pots := s.Side + if len(pots) == 0 { + all := make([]int, 0, len(live)) + for _, e := range live { + all = append(all, e.seat) + } + pots = []Pot{{Amount: s.Pot, Eligible: all}} + s.Pot = 0 + } + s.Side = nil + + for _, pot := range pots { + s.payPot(pot, live, evs) + } + s.endHand(evs) +} + +// payPot rakes a pot and splits it between the best eligible hands. +// +// The rake comes out of the pot before it is split, which is what a cardroom +// does and is also the only thing consistent with the rest of this casino: a +// player pays it out of a pot they *win*, never out of a bet they lose. A hand +// that dies before the flop is not raked at all — no flop, no drop — so folding +// your blind round after round costs you exactly the blinds and no fee. +func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) { + if pot.Amount <= 0 { + return + } + + amount := pot.Amount + if s.Flopped { + rake := int64(math.Floor(float64(amount) * s.Tier.RakePct)) + if cap := s.Tier.BB * rakeCapBB; rake > cap { + rake = cap + } + if rake > 0 { + amount -= rake + s.Rake += rake + *evs = append(*evs, Event{Kind: "rake", Seat: -1, Amount: rake}) + } + } + + eligible := make(map[int]bool, len(pot.Eligible)) + for _, seat := range pot.Eligible { + eligible[seat] = true + } + + var winners []ranked + best := int32(0) + for _, e := range live { + if !eligible[e.seat] { + continue + } + if len(winners) == 0 || e.rank < best { + best, winners = e.rank, []ranked{e} + } else if e.rank == best { + winners = append(winners, e) + } + } + if len(winners) == 0 { + return + } + + share := amount / int64(len(winners)) + odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button + for i, w := range winners { + won := share + if i == 0 { + won += odd + } + s.Seats[w.seat].Stack += won + s.Seats[w.seat].Won += won + *evs = append(*evs, Event{Kind: "win", Seat: w.seat, Amount: won, Text: w.desc}) + } +} + +// takeit ends a hand nobody contested: everyone else folded, so the last player +// standing takes the pot without showing. Their own uncalled bet comes back +// first — it was never called, so it was never really in the pot. +func (s *State) takeit(evs *[]Event) { + s.uncalled(evs) + s.collect() + *evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()}) + + winner := -1 + for i := range s.Seats { + if s.Seats[i].State != Folded && s.Seats[i].State != Out { + winner = i + break + } + } + if winner < 0 { + s.endHand(evs) + return + } + + // There are never side pots here: they are only cut once the betting is over + // because everybody is all-in, and a table where everybody is all-in is a table + // where nobody is left to fold. + pot := Pot{Amount: s.Pot, Eligible: []int{winner}} + s.Pot = 0 + s.payPot(pot, []ranked{{seat: winner, rank: 0}}, evs) + s.endHand(evs) +} + +// uncalled returns the unmatched top of a bet. If you shove 500 into a player +// with 200 behind, 300 of that was never contested and comes straight back. +// +// It must run *before* the bets are swept into the pot, and the matched level it +// measures against counts the players who folded. Their chips are in the pot — +// they paid to see the bet and then gave up — so the money they put in is money +// that called. Miss that and a bet folded to on the river comes back whole, +// including the part that was called on the flop, which mints chips out of air. +// +// The rake is the other reason this matters at all. When everybody folds, the +// winner takes the pot back either way and the arithmetic looks the same — but a +// pot with an uncalled bet still in it is a pot the house rakes, and it would be +// raking the player on their own money that nobody ever contested. +func (s *State) uncalled(evs *[]Event) { + top, topSeat := int64(-1), -1 + for i := range s.Seats { + p := &s.Seats[i] + if p.State == Folded || p.State == Out { + continue + } + if p.Total > top { + top, topSeat = p.Total, i + } + } + if topSeat < 0 { + return + } + + var matched int64 // the most anybody else put in, whether or not they're still in + for i := range s.Seats { + if i == topSeat || s.Seats[i].State == Out { + continue + } + if s.Seats[i].Total > matched { + matched = s.Seats[i].Total + } + } + + excess := top - matched + p := &s.Seats[topSeat] + if excess <= 0 || excess > p.Bet { + // An uncalled bet is always part of the street it was made on, so it cannot + // be bigger than what that seat has in front of them right now. + return + } + + p.Stack += excess + p.Total -= excess + p.Bet -= excess + if p.State == AllIn && p.Stack > 0 { + p.State = Active // they were never really all-in against anybody + } + *evs = append(*evs, Event{Kind: "uncalled", Seat: topSeat, Amount: excess}) +} diff --git a/internal/games/holdem/holdem.go b/internal/games/holdem/holdem.go new file mode 100644 index 0000000..89dcb10 --- /dev/null +++ b/internal/games/holdem/holdem.go @@ -0,0 +1,846 @@ +// Package holdem is a pure Texas Hold'em engine, played for chips against bots. +// +// Same seam as every other table in the casino: ApplyMove(state, move) (state, +// events, error), where an error means the move was illegal and nothing else. +// No HTTP, no timers, no sockets. The state is a plain value, so a hand survives +// a redeploy and replays from its seed. +// +// Three things make hold'em different from the five tables already on the felt. +// +// It is a cash game, not a stake. Every other game here takes a bet, plays once +// and pays a multiple. Poker isn't that: you buy chips onto the table, you play +// as many hands as you like, and you leave with whatever is in front of you. So +// the session is the unit, not the hand — the live row lives across hands, the +// buy-in is the only chip movement at the start, and the stack going home is the +// only one at the end. In between the money is entirely inside this engine. +// +// The bots move inside ApplyMove, as they do in UNO. One call plays the player's +// action and every bot action behind it, deals whatever streets that completes, +// and hands the lot back as a script of events. Poker is where you would reach +// for a socket, and this is what not reaching for one costs. +// +// The bots are the trained ones. gogobee spent a long time running CFR against +// this game and the policy it converged on is the best asset in either repo; it +// is embedded here whole (see cfr.go). What that means for the player: the house +// edge at this table is not a rule, it is an opponent. There is no 3:2 and no +// multiple. If you beat the bots you win, and the only thing the house takes is +// the rake on the pots you win. +package holdem + +import ( + "errors" + "math/rand/v2" + + "pete/internal/games/cards" +) + +// Errors an illegal move can produce. An error means nothing happened. +var ( + ErrOver = errors.New("holdem: you've left the table") + ErrNotYourTurn = errors.New("holdem: it isn't your turn") + ErrHandLive = errors.New("holdem: finish the hand first") + ErrNoHand = errors.New("holdem: no hand in progress") + ErrCantCheck = errors.New("holdem: there's a bet to you") + ErrNothingToCall = errors.New("holdem: nothing to call") + ErrTooSmall = errors.New("holdem: that's under the minimum raise") + ErrTooBig = errors.New("holdem: you don't have that many chips") + ErrNoChips = errors.New("holdem: you have no chips left") + ErrUnknownMove = errors.New("holdem: unknown move") + ErrUnknownTier = errors.New("holdem: no such table") + ErrBadBuyIn = errors.New("holdem: that isn't a legal buy-in") + ErrTableFull = errors.New("holdem: too many seats") +) + +// You are always seat zero. The bots are the seats after you. +const You = 0 + +// rakeCapBB caps the rake on any one pot at three big blinds, which is what a +// cardroom does. Without a cap, five percent of a big pot is a lot of money to +// take off a player for winning it. +const rakeCapBB = 3 + +// Street is how far the board has come. +type Street uint8 + +const ( + PreFlop Street = iota + Flop + Turn + River + Showdown +) + +var streetNames = [5]string{"preflop", "flop", "turn", "river", "showdown"} + +func (s Street) String() string { + if int(s) >= len(streetNames) { + return "?" + } + return streetNames[s] +} + +// SeatState is where a player stands in the hand being played. +type SeatState uint8 + +const ( + Active SeatState = iota // still has chips and a say + Folded // out of this hand + AllIn // in the hand, but has nothing left to bet + Out // not dealt in at all +) + +// Seat is one player at the table — you, or one of the bots. +// +// Hole is on the server and stays there. The view layer sends your two cards to +// you and sends nobody else's to anybody, right up until a showdown turns them +// over. A bot's cards are most of the information in this game; a browser that +// held them would make counting cards a matter of reading the network tab. +type Seat struct { + Name string `json:"name"` + Bot bool `json:"bot"` + Stack int64 `json:"stack"` + Hole [2]cards.Card `json:"hole"` + Bet int64 `json:"bet"` // put in on this street + Total int64 `json:"total"` // put in across this hand + Won int64 `json:"won"` // taken out of the pot this hand + State SeatState `json:"state"` + Acted bool `json:"acted"` // has chosen to do something this street +} + +// Pot is a pot and the seats that may win it. A hand with no all-in has exactly +// one; every all-in at a distinct level adds another. +type Pot struct { + Amount int64 `json:"amount"` + Eligible []int `json:"eligible"` +} + +// Tier is a table you can sit at. The dial is the stakes, and the stakes are +// what make a chip mean something: at 25/50 a careless call costs more than a +// whole session at 1/2. +// +// The buy-in range is the standard 20 to 100 big blinds. Sitting down short is +// a real strategy (fewer decisions, less to lose) and sitting down deep is the +// other one, so the range is a choice and not a formality. +type Tier struct { + Slug string `json:"slug"` + Name string `json:"name"` + SB int64 `json:"sb"` + BB int64 `json:"bb"` + MinBuy int64 `json:"min_buy"` + MaxBuy int64 `json:"max_buy"` + RakePct float64 `json:"rake_pct"` + Blurb string `json:"blurb"` +} + +// Tiers are the three tables. The rake is the casino's five percent everywhere, +// capped at three big blinds a pot, and taken only from a pot that saw a flop. +// +// RakePct is a *fraction*, 0.05, because that is what it is everywhere else in +// the casino — blackjack's DefaultRules says 0.05 and New() takes its word for it. +// It was 5 here for an afternoon, meaning percent, and since New overwrites the +// tier's value with the one it is handed, every rake worked out to five percent of +// a hundredth of the pot, which integer division rounded to nothing. The house took +// nothing at all and no test noticed, because every test set the tier up by hand. +var Tiers = []Tier{ + {Slug: "micro", Name: "The Kitchen Table", SB: 1, BB: 2, MinBuy: 40, MaxBuy: 200, RakePct: 0.05, + Blurb: "1/2 blinds. Cheap enough to learn what the bots do to you."}, + {Slug: "low", Name: "The Back Room", SB: 5, BB: 10, MinBuy: 200, MaxBuy: 1000, RakePct: 0.05, + Blurb: "5/10. A bluff here costs real chips, which is the only reason a bluff works."}, + {Slug: "high", Name: "The High Roller", SB: 25, BB: 50, MinBuy: 1000, MaxBuy: 5000, RakePct: 0.05, + Blurb: "25/50. Three streets of this and you know whether you can play."}, +} + +// TierBySlug finds a table by the name the browser sent. +func TierBySlug(slug string) (Tier, error) { + for _, t := range Tiers { + if t.Slug == slug { + return t, nil + } + } + return Tier{}, ErrUnknownTier +} + +// MaxBots is five, which with you makes a six-handed table — the size most +// online poker is actually played at, and as many opponents as the felt can +// show without the cards getting too small to read. +const MaxBots = 5 + +// Phase is what the table is waiting for. +type Phase string + +const ( + PhaseBetting Phase = "betting" // a hand is live and it's your turn + PhaseHandOver Phase = "handover" // the hand is paid; deal, top up, or leave + PhaseDone Phase = "done" // you're up from the table; the stack goes home +) + +// State is the whole table. It never leaves the server: the deck is in here, +// and so is every bot's hand. +type State struct { + Tier Tier `json:"tier"` + Seats []Seat `json:"seats"` + Button int `json:"button"` + HandNo int `json:"hand_no"` + + Deck cards.Deck `json:"deck"` + Community []cards.Card `json:"community"` + Street Street `json:"street"` + Flopped bool `json:"flopped"` // this hand saw a flop, so its pot is rakeable + + Pot int64 `json:"pot"` + Side []Pot `json:"side,omitempty"` + Bet int64 `json:"bet"` // the bet to match on this street + MinRaise int64 `json:"min_raise"` // the smallest legal raise over it + Aggressor int `json:"aggressor"` + ToAct int `json:"to_act"` + + // History is the action so far on this street, as the f/c/r/R/a characters + // the CFR policy was trained to read. It is the bots' memory and nothing else. + History string `json:"history"` + + Phase Phase `json:"phase"` + + // The money that crosses the border. BoughtIn is every chip staked onto this + // table; Payout is the stack that goes home, and is only set once you're up. + BoughtIn int64 `json:"bought_in"` + Payout int64 `json:"payout"` + Rake int64 `json:"rake"` // taken by the house across the whole session + + // The seed rides in the state for the same reason it does in UNO: the bots + // choose and the deck is reshuffled every hand, so the engine needs randomness + // mid-session — and there is no generator alive between two HTTP requests to + // hand it. Each step derives its own from the seed and the step count, so the + // session still replays exactly as it fell. + Seed1 uint64 `json:"seed1"` + Seed2 uint64 `json:"seed2"` + Step uint64 `json:"step"` +} + +// Event is one beat of the script the felt plays back. Seat is -1 when the beat +// belongs to the table rather than a player. +type Event struct { + Kind string `json:"kind"` + Seat int `json:"seat"` + Cards []cards.Card `json:"cards,omitempty"` + Amount int64 `json:"amount,omitempty"` + Total int64 `json:"total,omitempty"` + Text string `json:"text,omitempty"` +} + +// The moves a player can make. The betting five, plus the three that are about +// the session rather than the hand. +const ( + Fold = "fold" + Check = "check" + Call = "call" + Raise = "raise" + Shove = "allin" // the move; AllIn is the seat state it puts you in + + Deal = "deal" // next hand + TopUp = "topup" // put more chips on the table, between hands + Leave = "leave" // get up; the stack goes back to your stack +) + +// Move is what the browser sends. To is the total a raise raises *to*, and +// Amount is chips added in a top-up. +type Move struct { + Kind string `json:"move"` + To int64 `json:"to,omitempty"` + Amount int64 `json:"amount,omitempty"` +} + +// botNames are the regulars. Six of them so a full table never has two. +var botNames = []string{"Dice", "Marjorie", "Ox", "Sunny", "Pinch", "The Reverend"} + +// New sits you down. The buy-in is chips the caller has already taken off the +// player's stack; this engine only ever gives them back through Leave. +// +// No hand is dealt yet — the table opens on PhaseHandOver, which is the state a +// table between hands is in, and the first Deal move starts the first hand. +func New(t Tier, bots int, buyIn int64, rakePct float64, seed1, seed2 uint64) (State, []Event, error) { + if bots < 1 || bots > MaxBots { + return State{}, nil, ErrTableFull + } + if buyIn < t.MinBuy || buyIn > t.MaxBuy { + return State{}, nil, ErrBadBuyIn + } + t.RakePct = rakePct + + s := State{ + Tier: t, + Button: 0, + Phase: PhaseHandOver, + BoughtIn: buyIn, + Seed1: seed1, + Seed2: seed2, + } + s.Seats = append(s.Seats, Seat{Name: "You", Stack: buyIn}) + for i := 0; i < bots; i++ { + s.Seats = append(s.Seats, Seat{Name: botNames[i], Bot: true, Stack: t.MaxBuy}) + } + + // The button starts to your right, so the first hand deals you the small blind + // heads-up and the button on a full table — either way you are in the action + // from the first card rather than folding your way in. + s.Button = len(s.Seats) - 1 + + evs := []Event{{Kind: "sit", Seat: You, Amount: buyIn, Text: t.Name}} + return s, evs, nil +} + +// ApplyMove is the whole engine. It plays your move, then every bot behind you, +// deals whatever streets that finishes, and stops either when it is your turn +// again or when the hand is over. +func ApplyMove(s State, m Move) (State, []Event, error) { + if s.Phase == PhaseDone { + return s, nil, ErrOver + } + rng := s.next() + evs := []Event{} + + switch m.Kind { + case Deal: + if s.Phase != PhaseHandOver { + return s, nil, ErrHandLive + } + s.deal(&evs, rng) + s.advance(&evs, rng, true) + + case TopUp: + if s.Phase != PhaseHandOver { + return s, nil, ErrHandLive + } + if m.Amount <= 0 || s.Seats[You].Stack+m.Amount > s.Tier.MaxBuy { + return s, nil, ErrBadBuyIn + } + s.Seats[You].Stack += m.Amount + s.BoughtIn += m.Amount + evs = append(evs, Event{Kind: "topup", Seat: You, Amount: m.Amount}) + + case Leave: + if s.Phase != PhaseHandOver { + return s, nil, ErrHandLive + } + s.Phase = PhaseDone + s.Payout = s.Seats[You].Stack + evs = append(evs, Event{Kind: "leave", Seat: You, Amount: s.Payout}) + + case Fold, Check, Call, Raise, Shove: + if s.Phase != PhaseBetting { + return s, nil, ErrNoHand + } + if s.ToAct != You { + return s, nil, ErrNotYourTurn + } + if err := s.act(You, m, &evs); err != nil { + return s, nil, err // nothing happened; the caller keeps the old state + } + s.ToAct = s.nextCanAct(You) + s.advance(&evs, rng, true) + + default: + return s, nil, ErrUnknownMove + } + + return s, evs, nil +} + +// Step plays a move for whoever is to act — bot or player — and advances only as +// far as the next decision, whoever's it is. Nobody's turn is taken for them. +// +// This is the seam the trainer plays through, and it exists so that the trainer +// is playing *this* game: the same betting rules, the same street completion, the +// same side pots, the same money. The alternative is a second, simplified model +// of poker written for the trainer alone — which is what gogobee had, and it is +// why its policy encoded a game nobody was dealing. +func Step(s State, m Move) (State, []Event, error) { + if s.Phase != PhaseBetting { + return s, nil, ErrNoHand + } + rng := s.next() + evs := []Event{} + + seat := s.ToAct + if err := s.act(seat, m, &evs); err != nil { + return s, nil, err + } + s.ToAct = s.nextCanAct(seat) + s.advance(&evs, rng, false) + return s, evs, nil +} + +// Open deals one heads-up hand at the given stacks, stopping at the first +// decision. For the trainer: a table with no history and no button rotation. +func Open(t Tier, stack0, stack1 int64, seed1, seed2 uint64) (State, error) { + if stack0 <= 0 || stack1 <= 0 { + return State{}, ErrBadBuyIn + } + s := State{ + Tier: t, + Phase: PhaseHandOver, + Seats: []Seat{ + {Name: "0", Stack: stack0}, + {Name: "1", Stack: stack1, Bot: true}, + }, + Button: 1, // deal() moves it, so seat 0 takes the button and the small blind + Seed1: seed1, + Seed2: seed2, + } + rng := s.next() + evs := []Event{} + s.deal(&evs, rng) + s.advance(&evs, rng, false) + return s, nil +} + +// Clone deep-copies the table. CFR walks a tree of what-ifs, and a shallow copy +// would have every branch writing into the same deck. +func (s State) Clone() State { + out := s + out.Seats = append([]Seat(nil), s.Seats...) + out.Deck = append(cards.Deck(nil), s.Deck...) + out.Community = append([]cards.Card(nil), s.Community...) + out.Side = make([]Pot, len(s.Side)) + for i, p := range s.Side { + out.Side[i] = Pot{Amount: p.Amount, Eligible: append([]int(nil), p.Eligible...)} + } + return out +} + +// act applies one seat's action, whoever it belongs to. +func (s *State) act(seat int, m Move, evs *[]Event) error { + switch m.Kind { + case Fold: + s.fold(seat, evs) + return nil + case Check: + return s.check(seat, evs) + case Call: + return s.call(seat, evs) + case Raise: + return s.raise(seat, m.To, evs) + case Shove: + return s.allin(seat, evs) + } + return ErrUnknownMove +} + +// advance runs the table forward until you have a decision to make, or until +// there is nothing left to decide. +// +// This is the loop the whole design turns on. Every other engine here returns +// after one move because there is nobody else at the table; this one keeps going +// — bot, bot, flop, bot, turn — and only stops when the answer has to come from +// the player. Which is why one HTTP request can be a whole hand: shove all-in +// and the board runs out and the pot is paid inside a single call. +// +// With bots false it stops at every decision instead of playing the bots' for +// them. That is the trainer's way in: it wants to choose both seats' moves. +func (s *State) advance(evs *[]Event, rng *rand.Rand, bots bool) { + for { + // Everyone else folded. Nobody shows; the last one standing takes it. + if s.liveCount() <= 1 { + s.takeit(evs) + return + } + + // Nobody left with a decision to make: the rest of the board is a formality, + // so deal it and turn the cards over. + // + // The subtle half is the lone player who still has chips. They only have a + // decision if there is a bet to them — call it or fold. If there isn't, they + // have nobody left to bet *into*, because everyone else is already all-in, + // and poker does not let you put chips in a pot nobody can contest. + switch s.canActCount() { + case 0: + s.runout(evs) + return + case 1: + lone := s.onlyActor() + if s.Owed(lone) == 0 { + s.runout(evs) + return + } + s.ToAct = lone + } + + if s.streetDone(s.ToAct) { + if s.Street == River { + s.showdown(evs) + return + } + s.street(evs) + continue + } + + if s.ToAct == You || !bots { + return // a decision that isn't ours to make + } + + s.botActs(s.ToAct, evs, rng) + s.ToAct = s.nextCanAct(s.ToAct) + } +} + +// deal starts a hand: rebuy the broke bots, move the button, shuffle, post the +// blinds, and put two cards in front of everybody. +func (s *State) deal(evs *[]Event, rng *rand.Rand) { + s.HandNo++ + + for i := range s.Seats { + p := &s.Seats[i] + // A bot that has been ground down to nothing reloads. It has to: a table + // where you have taken everybody's chips is a table with no game left in it, + // and their chips were never real anyway — the only real money at this table + // is yours, and the only thing the house takes is the rake. + if p.Bot && p.Stack < s.Tier.BB { + add := s.Tier.MaxBuy - p.Stack + p.Stack = s.Tier.MaxBuy + *evs = append(*evs, Event{Kind: "rebuy", Seat: i, Amount: add, Total: p.Stack}) + } + p.Bet, p.Total, p.Won, p.Acted = 0, 0, 0, false + p.Hole = [2]cards.Card{} + p.State = Active + if p.Stack <= 0 { + p.State = Out + } + } + + s.Community = nil + s.Side = nil + s.Pot = 0 + s.Street = PreFlop + s.Flopped = false + s.History = "" + s.Phase = PhaseBetting + + s.Deck = cards.NewDeck(1) + s.Deck.Shuffle(rng) + + s.Button = s.nextIn(s.Button) + *evs = append(*evs, Event{Kind: "hand", Seat: s.Button, Amount: int64(s.HandNo)}) + + bb := s.blinds(evs) + + // Two cards each, one at a time round the table, as they are actually dealt. + for round := 0; round < 2; round++ { + for i := 0; i < len(s.Seats); i++ { + seat := (s.Button + 1 + i) % len(s.Seats) + p := &s.Seats[seat] + if p.State == Out { + continue + } + c, _ := s.Deck.Draw() + p.Hole[round] = c + } + } + // Only your cards go into the script. The bots' are in the state, on this side + // of the wire, and the only thing that ever turns them over is a showdown. + *evs = append(*evs, Event{Kind: "hole", Seat: You, + Cards: []cards.Card{s.Seats[You].Hole[0], s.Seats[You].Hole[1]}}) + + s.ToAct = s.firstPreFlop(bb) +} + +// street burns one and deals the next board card or three. +func (s *State) street(evs *[]Event) { + s.collect() + s.resetBets() + + s.Deck.Draw() // the burn card, as printed in the rules and as dealt in a casino + + switch s.Street { + case PreFlop: + s.Street = Flop + s.Flopped = true + for i := 0; i < 3; i++ { + c, _ := s.Deck.Draw() + s.Community = append(s.Community, c) + } + case Flop: + s.Street = Turn + c, _ := s.Deck.Draw() + s.Community = append(s.Community, c) + case Turn: + s.Street = River + c, _ := s.Deck.Draw() + s.Community = append(s.Community, c) + } + + // Total, not Pot: by the time a board runs out behind an all-in the money has + // already been cut into side pots, and s.Pot is zero. + *evs = append(*evs, Event{Kind: s.Street.String(), Seat: -1, + Cards: s.Community[len(s.Community)-cardsOn(s.Street):], Amount: s.Total()}) + + s.ToAct = s.firstPostFlop() + s.Aggressor = s.ToAct // nobody has bet yet, so the option ends where it starts +} + +func cardsOn(st Street) int { + if st == Flop { + return 3 + } + return 1 +} + +// runout deals the rest of the board with no more betting, because there is no +// longer anybody able to bet. The side pots are built first: once the chips stop +// moving, who can win what is already decided. +func (s *State) runout(evs *[]Event) { + allIn := false + for i := range s.Seats { + if s.Seats[i].State == AllIn { + allIn = true + break + } + } + if allIn { + // A shove nobody could cover comes back first — while it is still a bet in + // front of a seat, and before the pots are cut around it. + s.uncalled(evs) + } + s.collect() + if allIn { + s.sidePots() + } + + for s.Street < River { + s.street(evs) + } + s.showdown(evs) +} + +// endHand pays out and parks the table between hands. +func (s *State) endHand(evs *[]Event) { + s.Pot = 0 + s.Side = nil + s.Phase = PhaseHandOver + *evs = append(*evs, Event{Kind: "end", Seat: -1, Amount: s.Seats[You].Stack}) + + // Busting is the end of the session, not the end of a hand. There is nothing + // to deal you and nothing to give back, so the table closes and you sit down + // again — which is a buy-in, and a buy-in is a decision worth making on purpose. + if s.Seats[You].Stack <= 0 { + s.Phase = PhaseDone + s.Payout = 0 + *evs = append(*evs, Event{Kind: "bust", Seat: You}) + } +} + +// ---- the small stuff ------------------------------------------------------- + +// next derives this step's generator and advances the step count. +func (s *State) next() *rand.Rand { + s.Step++ + return cards.NewRNG(s.Seed1, s.Seed2^s.Step) +} + +// collect sweeps the street's bets into the pot. +func (s *State) collect() { + for i := range s.Seats { + s.Pot += s.Seats[i].Bet + s.Seats[i].Bet = 0 + } +} + +// resetBets opens a new street: nothing to call, and nobody has spoken. +func (s *State) resetBets() { + for i := range s.Seats { + s.Seats[i].Bet = 0 + s.Seats[i].Acted = false + } + s.Bet = 0 + s.MinRaise = s.Tier.BB + s.History = "" +} + +// inPlay is the pot plus everything bet on this street — what a bot is actually +// deciding against, and what a pot-sized raise is a size of. +func (s State) inPlay() int64 { + total := s.Pot + for i := range s.Seats { + total += s.Seats[i].Bet + } + return total +} + +// Pot returns the money on the table, however it is currently sliced. +func (s State) Total() int64 { + total := s.inPlay() + for _, p := range s.Side { + total += p.Amount + } + return total +} + +// liveCount is the seats still in the hand, whether or not they can bet. +func (s State) liveCount() int { + n := 0 + for i := range s.Seats { + if st := s.Seats[i].State; st == Active || st == AllIn { + n++ + } + } + return n +} + +// canActCount is the seats that still have chips and a decision. +func (s State) canActCount() int { + n := 0 + for i := range s.Seats { + if s.Seats[i].State == Active { + n++ + } + } + return n +} + +// dealt is the seats in this hand at all. +func (s State) dealt() int { + n := 0 + for i := range s.Seats { + if s.Seats[i].State != Out { + n++ + } + } + return n +} + +// nextIn is the next seat that was dealt in — used to move the button, which +// moves past a seat that is sitting out rather than landing on it. +func (s State) nextIn(from int) int { + n := len(s.Seats) + for i := 1; i <= n; i++ { + next := (from + i) % n + if st := s.Seats[next].State; st == Active || st == AllIn { + return next + } + } + return from +} + +// onlyActor is the one seat that can still act. Call it when canActCount is 1. +func (s State) onlyActor() int { + for i := range s.Seats { + if s.Seats[i].State == Active { + return i + } + } + return s.ToAct +} + +// canBet reports whether there is anybody left to bet *into*. With one player +// still holding chips and the rest all-in, a raise is chips nobody can call, so +// no raise is offered — to the player or to a bot. +func (s State) canBet() bool { return s.canActCount() > 1 } + +// nextCanAct is the next seat with a decision to make. +func (s State) nextCanAct(from int) int { + n := len(s.Seats) + for i := 1; i <= n; i++ { + next := (from + i) % n + if s.Seats[next].State == Active { + return next + } + } + return from +} + +// Owed is what the seat must put in to call. +func (s State) Owed(seat int) int64 { + owed := s.Bet - s.Seats[seat].Bet + if owed < 0 { + return 0 + } + if owed > s.Seats[seat].Stack { + return s.Seats[seat].Stack + } + return owed +} + +// MinRaiseTo is the smallest total a raise may raise to, clamped to a shove when +// the seat cannot cover a full one. +func (s State) MinRaiseTo(seat int) int64 { + to := s.Bet + s.MinRaise + if most := s.Seats[seat].Bet + s.Seats[seat].Stack; to > most { + return most + } + return to +} + +// MaxRaiseTo is everything the seat has. +func (s State) MaxRaiseTo(seat int) int64 { + return s.Seats[seat].Bet + s.Seats[seat].Stack +} + +// CanRaise reports whether the seat may raise: they need chips behind the call, +// and somebody left to bet into. The felt asks so it never offers a button the +// table would refuse. +func (s State) CanRaise(seat int) bool { + return s.mask(seat)[actRaiseHalf] +} + +// InPosition reports whether the seat acts last after the flop, which is the +// only thing about position the trained bots actually know. +// +// The postflop order runs from the seat left of the button all the way round to +// the button itself, so the player in position is simply the last one still in +// the hand — the button, or whoever is nearest to it once the button has folded. +// The policy was trained heads-up, where this is exactly the button; applying it +// to a six-handed table is an approximation, and this is where the approximation +// lives. +func (s State) InPosition(seat int) bool { + last := -1 + for i := 1; i <= len(s.Seats); i++ { + at := (s.Button + i) % len(s.Seats) + if st := s.Seats[at].State; st == Active || st == AllIn { + last = at + } + } + return seat == last +} + +// Position is the seat's label at this table — BTN, SB, BB, and so on. It is for +// the felt to print. The bots do not use it: see InPosition, and the note on +// infoSet about what happens when you confuse the two. +func (s State) Position(seat int) string { + n := s.dealt() + if n < 2 { + return "" + } + if seat == s.Button { + return "BTN" + } + if n == 2 { + return "BB" // heads-up, the other seat is always the big blind + } + + sb := s.nextIn(s.Button) + bb := s.nextIn(sb) + switch seat { + case sb: + return "SB" + case bb: + return "BB" + } + + utg := s.nextIn(bb) + if seat == utg { + return "UTG" + } + + // Everyone between UTG and the button is somewhere in the middle; the seat + // closest to the button is the cutoff. + dist, cur := 0, utg + for i := 0; i < n; i++ { + cur = s.nextIn(cur) + dist++ + if cur == seat { + break + } + } + if remaining := n - 4; remaining > 0 && dist >= remaining { + return "CO" + } + return "MP" +} diff --git a/internal/games/holdem/holdem_test.go b/internal/games/holdem/holdem_test.go new file mode 100644 index 0000000..cc984e4 --- /dev/null +++ b/internal/games/holdem/holdem_test.go @@ -0,0 +1,641 @@ +package holdem + +import ( + "math/rand/v2" + "testing" + + "pete/internal/games/cards" +) + +// The one that matters: no chip is ever created or destroyed. +// +// Everything else in this package is a rule you could argue about. This is the +// one that would lose somebody money. Every chip at the table is in exactly one +// place — a stack, a bet in front of a seat, a pot, or the house's rake — and +// the only thing that ever adds to the total is a bot reloading. So: play a +// hundred sessions of real hands, with the trained bots making real decisions, +// and count the chips after every single move. +func TestChipsAreConserved(t *testing.T) { + for game := 0; game < 100; game++ { + rng := rand.New(rand.NewPCG(uint64(game), 99)) + bots := 1 + game%MaxBots + tier := Tiers[game%len(Tiers)] + + s, _, err := New(tier, bots, tier.MaxBuy, tier.RakePct, uint64(game), 7) + if err != nil { + t.Fatalf("new table: %v", err) + } + + want := chipsAt(s) // what the table started with + + for hand := 0; hand < 8 && s.Phase != PhaseDone; hand++ { + var evs []Event + s, evs, err = ApplyMove(s, Move{Kind: Deal}) + if err != nil { + t.Fatalf("game %d hand %d: deal: %v", game, hand, err) + } + want += reloaded(evs) // a bot that rebought brought new chips with it + check(t, s, want, game, hand, "deal") + + for step := 0; s.Phase == PhaseBetting; step++ { + if step > 200 { + t.Fatalf("game %d hand %d: the hand will not end", game, hand) + } + s, _, err = ApplyMove(s, randomMove(s, rng)) + if err != nil { + t.Fatalf("game %d hand %d: %v", game, hand, err) + } + check(t, s, want, game, hand, "move") + } + } + } +} + +// chipsAt totals every chip the table can see, plus every one the house has +// already taken out of it. +func chipsAt(s State) int64 { + total := s.Rake + s.Pot + for _, p := range s.Seats { + total += p.Stack + p.Bet + } + for _, pot := range s.Side { + total += pot.Amount + } + return total +} + +// reloaded is what the bots brought back to the table on this deal. It is the +// only thing in the game that is allowed to make chips out of nothing, which is +// exactly why the test has to know about it and nothing else does. +func reloaded(evs []Event) int64 { + var n int64 + for _, e := range evs { + if e.Kind == "rebuy" { + n += e.Amount + } + } + return n +} + +func check(t *testing.T, s State, want int64, game, hand int, when string) { + t.Helper() + if got := chipsAt(s); got != want { + t.Fatalf("game %d hand %d, after %s: %d chips on the table, want %d "+ + "(pot %d, rake %d, stacks %v)", game, hand, when, got, want, s.Pot, s.Rake, stacks(s)) + } + for i, p := range s.Seats { + if p.Stack < 0 { + t.Fatalf("game %d hand %d: seat %d has a negative stack (%d)", game, hand, i, p.Stack) + } + } +} + +func stacks(s State) []int64 { + out := make([]int64, len(s.Seats)) + for i, p := range s.Seats { + out[i] = p.Stack + } + return out +} + +// randomMove picks something legal for the player, without any thought at all. +// A bad player is exactly what this test wants: it gets into all-ins, folds, +// short stacks and split pots far faster than a good one would. +func randomMove(s State, rng *rand.Rand) Move { + owed := s.Owed(You) + var legal []Move + if owed > 0 { + legal = append(legal, Move{Kind: Fold}, Move{Kind: Call}) + } else { + legal = append(legal, Move{Kind: Check}) + } + if s.Seats[You].Stack > owed && s.canBet() { + legal = append(legal, Move{Kind: Shove}) + if to := s.MinRaiseTo(You); to < s.MaxRaiseTo(You) { + legal = append(legal, Move{Kind: Raise, To: to}) + } + } + return legal[rng.IntN(len(legal))] +} + +// ---- the rules a poker player would notice were wrong ----------------------- + +func TestHeadsUpButtonIsTheSmallBlindAndActsFirst(t *testing.T) { + s := table(t, Tiers[0], 1, 200) + s, evs, err := ApplyMove(s, Move{Kind: Deal}) + if err != nil { + t.Fatal(err) + } + + // The button posted the small blind, not the big one. + var small, big int + for _, e := range evs { + if e.Kind == "blind" && e.Text == "small" { + small = e.Seat + } + if e.Kind == "blind" && e.Text == "big" { + big = e.Seat + } + } + if small != s.Button { + t.Errorf("heads-up: seat %d posted the small blind, but the button is seat %d", small, s.Button) + } + if big == s.Button { + t.Error("heads-up: the button posted the big blind too") + } + // And it is the first to act before the flop. (If the button is a bot it has + // already acted, so what we can check is that the player didn't get skipped.) + if s.Phase != PhaseBetting { + t.Fatalf("phase %q: the hand should be waiting on somebody", s.Phase) + } +} + +func TestTheBigBlindGetsTheirOption(t *testing.T) { + // A table where everyone just calls: the big blind has the bet matched without + // ever having chosen anything, and the street must not end until they speak. + s := table(t, Tiers[0], 1, 200) + s, _, _ = ApplyMove(s, Move{Kind: Deal}) + + // Find a hand where the player is the big blind. The button alternates, so at + // most a couple of deals. + for i := 0; i < 6 && s.Position(You) != "BB"; i++ { + s = playOut(t, s) + s, _, _ = ApplyMove(s, Move{Kind: Deal}) + } + if s.Position(You) != "BB" { + t.Skip("never dealt the big blind") + } + if s.Phase != PhaseBetting { + return // the bot folded or raised; either way the option isn't the question + } + if s.ToAct == You && s.Owed(You) == 0 { + // This is the option: nothing to call, but the hand is still ours to act on. + if _, _, err := ApplyMove(s, Move{Kind: Check}); err != nil { + t.Errorf("the big blind cannot check their option: %v", err) + } + } +} + +func TestAShortAllInDoesNotReopenTheBetting(t *testing.T) { + s := State{ + Tier: Tiers[1], // 5/10 + Seats: []Seat{{Name: "You", Stack: 1000}, {Name: "Bot", Bot: true, Stack: 1000}}, + Bet: 100, + MinRaise: 100, // a full raise would be to 200 + Aggressor: You, + Phase: PhaseBetting, + } + s.Seats[You].Bet = 100 + s.Seats[1].Bet = 0 + s.Seats[1].Stack = 150 // can only get to 150, which is a raise of 50: not a full one + + var evs []Event + if err := s.allin(1, &evs); err != nil { + t.Fatal(err) + } + if s.Bet != 150 { + t.Errorf("the bet to call is %d, want 150", s.Bet) + } + if s.MinRaise != 100 { + t.Errorf("min raise moved to %d — a short all-in must not change it", s.MinRaise) + } + if s.Aggressor != You { + t.Errorf("the aggressor moved to seat %d — a short all-in must not reopen the action", s.Aggressor) + } +} + +func TestSidePotsPayInLayers(t *testing.T) { + // Three players all-in for different amounts. The short stack can only win + // what everyone could have lost to them. + s := State{ + Tier: Tiers[1], + Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}, {Name: "B", Bot: true}}, + } + s.Seats[0].Total, s.Seats[0].State = 100, AllIn // short + s.Seats[1].Total, s.Seats[1].State = 500, AllIn // middle + s.Seats[2].Total, s.Seats[2].State = 500, AllIn // covers + + s.sidePots() + + if len(s.Side) != 2 { + t.Fatalf("got %d pots, want 2: %+v", len(s.Side), s.Side) + } + main, side := s.Side[0], s.Side[1] + if main.Amount != 300 { // 100 from each of the three + t.Errorf("main pot is %d, want 300", main.Amount) + } + if len(main.Eligible) != 3 { + t.Errorf("main pot has %d eligible, want all 3", len(main.Eligible)) + } + if side.Amount != 800 { // 400 more from each of the two who had it + t.Errorf("side pot is %d, want 800", side.Amount) + } + if len(side.Eligible) != 2 { + t.Errorf("side pot has %d eligible, want 2 — the short stack cannot win it", len(side.Eligible)) + } + if total := main.Amount + side.Amount; total != 1100 { + t.Errorf("the pots hold %d, but %d went in", total, 1100) + } +} + +func TestFoldedChipsStayInThePotButWinNothing(t *testing.T) { + s := State{ + Tier: Tiers[1], + Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}, {Name: "B", Bot: true}}, + } + s.Seats[0].Total, s.Seats[0].State = 200, AllIn + s.Seats[1].Total, s.Seats[1].State = 50, Folded // called 50 and gave up + s.Seats[2].Total, s.Seats[2].State = 200, AllIn + + s.sidePots() + + var total int64 + for _, p := range s.Side { + total += p.Amount + for _, seat := range p.Eligible { + if seat == 1 { + t.Error("a folded seat is eligible to win a pot") + } + } + } + if total != 450 { + t.Errorf("the pots hold %d, want 450 — the folder's 50 has to still be in there", total) + } +} + +func TestAnUncalledBetComesBack(t *testing.T) { + s := State{ + Tier: Tiers[1], + Seats: []Seat{{Name: "You", Stack: 0}, {Name: "A", Bot: true}}, + } + s.Seats[0].Total, s.Seats[0].Bet, s.Seats[0].State = 500, 500, AllIn + s.Seats[1].Total, s.Seats[1].State = 200, Folded + + var evs []Event + s.uncalled(&evs) + + if s.Seats[You].Stack != 300 { + t.Errorf("got %d back, want the 300 nobody called", s.Seats[You].Stack) + } + if s.Seats[You].Total != 200 { + t.Errorf("still committed for %d, want 200 — the rest was never in the pot", s.Seats[You].Total) + } + if s.Seats[You].State != Active { + t.Error("still marked all-in for chips that came back") + } +} + +// ---- the rake -------------------------------------------------------------- + +func TestNoFlopNoDrop(t *testing.T) { + s := State{Tier: Tiers[1], Flopped: false, Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} + var evs []Event + s.payPot(Pot{Amount: 1000, Eligible: []int{You}}, []ranked{{seat: You}}, &evs) + + if s.Rake != 0 { + t.Errorf("raked %d off a pot that never saw a flop", s.Rake) + } + if s.Seats[You].Stack != 1000 { + t.Errorf("paid %d of a 1000 pot", s.Seats[You].Stack) + } +} + +func TestTheRakeIsCapped(t *testing.T) { + s := State{Tier: Tiers[1], Flopped: true, Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} + var evs []Event + // 5% of 10,000 is 500, but the cap is three big blinds — 30 at 5/10. + s.payPot(Pot{Amount: 10000, Eligible: []int{You}}, []ranked{{seat: You}}, &evs) + + want := s.Tier.BB * rakeCapBB + if s.Rake != want { + t.Errorf("raked %d, want the %d cap", s.Rake, want) + } + if s.Seats[You].Stack != 10000-want { + t.Errorf("paid %d, want %d", s.Seats[You].Stack, 10000-want) + } +} + +func TestTheRakeIsFivePercentUnderTheCap(t *testing.T) { + s := State{Tier: Tiers[0], Flopped: true, Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} + var evs []Event + s.payPot(Pot{Amount: 100, Eligible: []int{You}}, []ranked{{seat: You}}, &evs) // cap is 6 at 1/2 + + if s.Rake != 5 { + t.Errorf("raked %d off a 100 pot, want 5", s.Rake) + } + if s.Seats[You].Stack != 95 { + t.Errorf("paid %d, want 95", s.Seats[You].Stack) + } +} + +// The rake has to survive the wiring, not only the arithmetic. +// +// This is the test that was missing, and a browser found what it would have +// found: New() overwrites the tier's rake with the one the casino hands it, and +// the casino hands it a *fraction* (blackjack's 0.05). The tier declared 5, +// meaning percent. Every other rake test builds a State by hand and sets the tier +// itself, so not one of them ever saw the number a real table runs on — and the +// house quietly took nothing from every pot for an afternoon. +func TestTheRakeSurvivesTheConstructor(t *testing.T) { + tier := Tiers[1] // 5/10, so the cap is 30 + s, _, err := New(tier, 1, tier.MaxBuy, 0.05, 1, 2) + if err != nil { + t.Fatal(err) + } + s.Flopped = true + + var evs []Event + s.payPot(Pot{Amount: 400, Eligible: []int{You}}, []ranked{{seat: You}}, &evs) + + if s.Rake != 20 { + t.Fatalf("the house took %d of a 400 pot, want 20 — five percent of it. "+ + "RakePct is a fraction (0.05), not a percentage (5): see the note on Tiers.", s.Rake) + } + if !has(evs, "rake") { + t.Error("the rake was taken with no event to say so, so the felt cannot show it") + } +} + +func TestASplitPotSplits(t *testing.T) { + s := State{Tier: Tiers[1], Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} + var evs []Event + // Same rank: they chop. The odd chip goes to one of them, not into the air. + s.payPot(Pot{Amount: 101, Eligible: []int{0, 1}}, + []ranked{{seat: 0, rank: 500}, {seat: 1, rank: 500}}, &evs) + + if got := s.Seats[0].Stack + s.Seats[1].Stack; got != 101 { + t.Errorf("paid out %d of a 101 pot", got) + } + if s.Seats[0].Stack != 51 || s.Seats[1].Stack != 50 { + t.Errorf("split %d/%d, want 51/50", s.Seats[0].Stack, s.Seats[1].Stack) + } +} + +// ---- the session ----------------------------------------------------------- + +func TestYouCannotWalkOutOfALiveHand(t *testing.T) { + s := table(t, Tiers[0], 2, 200) + s, _, _ = ApplyMove(s, Move{Kind: Deal}) + if s.Phase != PhaseBetting { + t.Skip("the hand ended before the player could act") + } + if _, _, err := ApplyMove(s, Move{Kind: Leave}); err != ErrHandLive { + t.Errorf("leaving mid-hand gave %v, want ErrHandLive", err) + } + if _, _, err := ApplyMove(s, Move{Kind: TopUp, Amount: 10}); err != ErrHandLive { + t.Errorf("topping up mid-hand gave %v, want ErrHandLive", err) + } +} + +func TestLeavingTakesTheStackHome(t *testing.T) { + s := table(t, Tiers[0], 1, 200) + s, _, _ = ApplyMove(s, Move{Kind: Deal}) + s = playOut(t, s) + + stack := s.Seats[You].Stack + s, _, err := ApplyMove(s, Move{Kind: Leave}) + if err != nil { + t.Fatal(err) + } + if s.Phase != PhaseDone { + t.Errorf("phase %q after leaving, want done", s.Phase) + } + if s.Payout != stack { + t.Errorf("payout %d, want the %d that was in front of us", s.Payout, stack) + } + if _, _, err := ApplyMove(s, Move{Kind: Deal}); err != ErrOver { + t.Errorf("dealt a hand at a table we got up from: %v", err) + } +} + +func TestBustingEndsTheSession(t *testing.T) { + s := table(t, Tiers[0], 1, 200) + s.Seats[You].Stack = 0 + var evs []Event + s.endHand(&evs) + + if s.Phase != PhaseDone { + t.Errorf("phase %q with no chips left, want done", s.Phase) + } + if s.Payout != 0 { + t.Errorf("payout %d for a busted player", s.Payout) + } + if !has(evs, "bust") { + t.Error("no bust event") + } +} + +func TestATopUpCannotGoOverTheTableMax(t *testing.T) { + s := table(t, Tiers[0], 1, 200) // max buy is 200, and we're at it + if _, _, err := ApplyMove(s, Move{Kind: TopUp, Amount: 1}); err != ErrBadBuyIn { + t.Errorf("topped up over the table maximum: %v", err) + } + + s = table(t, Tiers[0], 1, 100) + s, _, err := ApplyMove(s, Move{Kind: TopUp, Amount: 50}) + if err != nil { + t.Fatal(err) + } + if s.Seats[You].Stack != 150 { + t.Errorf("stack is %d, want 150", s.Seats[You].Stack) + } + if s.BoughtIn != 150 { + t.Errorf("bought in for %d, want 150 — the top-up is real money too", s.BoughtIn) + } +} + +func TestABuyInHasToBeInRange(t *testing.T) { + tier := Tiers[0] + for _, amount := range []int64{0, tier.MinBuy - 1, tier.MaxBuy + 1} { + if _, _, err := New(tier, 1, amount, 5, 1, 2); err != ErrBadBuyIn { + t.Errorf("buy-in of %d at a %d–%d table: %v", amount, tier.MinBuy, tier.MaxBuy, err) + } + } +} + +// ---- what the player is allowed to know ------------------------------------ + +func TestTheScriptNeverCarriesABotsCards(t *testing.T) { + rng := rand.New(rand.NewPCG(4, 4)) + s := table(t, Tiers[0], 3, 200) + + for hand := 0; hand < 20 && s.Phase != PhaseDone; hand++ { + s, evs, err := ApplyMove(s, Move{Kind: Deal}) + if err != nil { + t.Fatal(err) + } + noBotCards(t, s, evs) + + for s.Phase == PhaseBetting { + var next []Event + s, next, err = ApplyMove(s, randomMove(s, rng)) + if err != nil { + t.Fatal(err) + } + noBotCards(t, s, next) + } + } +} + +// A bot's cards may appear in exactly one kind of event: the showdown that turns +// them face up, which is the moment they stop being secret. +func noBotCards(t *testing.T, s State, evs []Event) { + t.Helper() + for _, e := range evs { + if len(e.Cards) == 0 || e.Seat < 0 || e.Kind == "show" { + continue + } + if e.Seat != You && s.Seats[e.Seat].Bot { + t.Fatalf("a %q event carries seat %d's cards (%v) — that's a bot's hand", + e.Kind, e.Seat, e.Cards) + } + } +} + +// ---- hand strength --------------------------------------------------------- + +func TestTheEvaluatorKnowsWhichHandIsBetter(t *testing.T) { + board := []cards.Card{ + {Rank: 10, Suit: cards.Hearts}, {Rank: cards.Jack, Suit: cards.Hearts}, + {Rank: cards.Queen, Suit: cards.Hearts}, {Rank: 2, Suit: cards.Spades}, + {Rank: 7, Suit: cards.Clubs}, + } + flush, _ := rankOf([2]cards.Card{{Rank: 3, Suit: cards.Hearts}, {Rank: 5, Suit: cards.Hearts}}, board) + straight, _ := rankOf([2]cards.Card{{Rank: cards.King, Suit: cards.Spades}, {Rank: cards.Ace, Suit: cards.Clubs}}, board) + royal, _ := rankOf([2]cards.Card{{Rank: cards.King, Suit: cards.Hearts}, {Rank: cards.Ace, Suit: cards.Hearts}}, board) + pair, _ := rankOf([2]cards.Card{{Rank: 7, Suit: cards.Spades}, {Rank: 4, Suit: cards.Diamonds}}, board) + + if !(royal < flush && flush < straight && straight < pair) { + t.Errorf("hands rank royal=%d flush=%d straight=%d pair=%d — lower must be better, in that order", + royal, flush, straight, pair) + } +} + +func TestEquityKnowsAcesAreGood(t *testing.T) { + rng := rand.New(rand.NewPCG(1, 1)) + aces := equityOf([2]cards.Card{{Rank: cards.Ace, Suit: cards.Spades}, {Rank: cards.Ace, Suit: cards.Hearts}}, nil, 1, 2000, rng) + rags := equityOf([2]cards.Card{{Rank: 7, Suit: cards.Spades}, {Rank: 2, Suit: cards.Hearts}}, nil, 1, 2000, rng) + + if aces.Strength() < 0.8 { + t.Errorf("pocket aces are worth %.2f heads-up, want about 0.85", aces.Strength()) + } + if rags.Strength() > 0.4 { + t.Errorf("seven-deuce is worth %.2f heads-up, want about 0.35", rags.Strength()) + } + if aces.Strength() <= rags.Strength() { + t.Error("seven-deuce is not better than pocket aces") + } +} + +// The policy loads, and every node in it is a probability distribution. +func TestThePolicyLoads(t *testing.T) { + p := loadPolicy() + if len(p) < 1000 { + t.Fatalf("the CFR policy has %d nodes in it — it did not load, or it was never trained", len(p)) + } + for key, probs := range p { + var sum float64 + for _, v := range probs { + if v < 0 { + t.Fatalf("%s: a negative probability (%v)", key, probs) + } + sum += v + } + if sum < 0.99 || sum > 1.01 { + t.Fatalf("%s: the probabilities sum to %v, not 1", key, sum) + } + } +} + +// TestTheBotsAreActuallyTrained is the test this game most needed and did not +// have. +// +// A bot that cannot find itself in the policy does not fail. It shrugs, plays the +// pot-odds rule, and looks exactly like a bot that is working — which is how +// gogobee shipped a trained poker AI whose policy was *never read once* for the +// entire life of the game. The trainer wrote its keys under IP/OOP and the table +// looked them up under BTN/SB/BB, and there was nothing anywhere that would have +// said so. +// +// So: deal real hands, let the bots think, and count how often the thinking lands +// in the table. Heads-up is the number that has to hold — that is what the policy +// was trained on. A six-handed table is a documented approximation of it and +// drops off as seats are added, which is why this only asserts on the duel. +func TestTheBotsAreActuallyTrained(t *testing.T) { + Hits.Store(0) + Misses.Store(0) + + rng := rand.New(rand.NewPCG(11, 12)) + for game := 0; game < 40; game++ { + tier := Tiers[1] + s, _, err := New(tier, 1, tier.MaxBuy, tier.RakePct, uint64(game), 5) + if err != nil { + t.Fatal(err) + } + for hand := 0; hand < 6 && s.Phase != PhaseDone; hand++ { + s, _, err = ApplyMove(s, Move{Kind: Deal}) + if err != nil { + t.Fatal(err) + } + for s.Phase == PhaseBetting { + s, _, err = ApplyMove(s, randomMove(s, rng)) + if err != nil { + t.Fatal(err) + } + } + } + } + + hits, misses := Hits.Load(), Misses.Load() + if hits+misses < 100 { + t.Fatalf("the bots only made %d decisions — this test isn't measuring anything", hits+misses) + } + rate := float64(hits) / float64(hits+misses) + if rate < 0.6 { + t.Fatalf("heads-up, the bots found themselves in the trained policy %.0f%% of the time "+ + "(%d of %d decisions). They are playing the pot-odds fallback, which means the key the "+ + "trainer writes and the key the table reads have drifted apart. See infoSet.", + rate*100, hits, hits+misses) + } + t.Logf("heads-up policy hit rate: %.0f%% (%d of %d decisions)", rate*100, hits, hits+misses) +} + +// ---- helpers --------------------------------------------------------------- + +func table(t *testing.T, tier Tier, bots int, buyIn int64) State { + t.Helper() + s, _, err := New(tier, bots, buyIn, tier.RakePct, 1, 2) + if err != nil { + t.Fatalf("new table: %v", err) + } + return s +} + +// playOut folds every decision until the hand is over. +func playOut(t *testing.T, s State) State { + t.Helper() + for i := 0; s.Phase == PhaseBetting; i++ { + if i > 100 { + t.Fatal("the hand will not end") + } + move := Move{Kind: Fold} + if s.Owed(You) == 0 { + move = Move{Kind: Check} + } + var err error + s, _, err = ApplyMove(s, move) + if err != nil { + t.Fatalf("playing out: %v", err) + } + } + return s +} + +func has(evs []Event, kind string) bool { + for _, e := range evs { + if e.Kind == kind { + return true + } + } + return false +} diff --git a/internal/games/holdem/policy.gob b/internal/games/holdem/policy.gob new file mode 100644 index 0000000000000000000000000000000000000000..329c286f12989caf2ae048deec5306ccc6de4217 GIT binary patch literal 203953 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 z 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

sG7=hZ{yy5orxsW=UT16O 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 Jtq>6Se*gml?Uw)m literal 0 HcmV?d00001 diff --git a/internal/games/holdem/train.go b/internal/games/holdem/train.go new file mode 100644 index 0000000..3c5930c --- /dev/null +++ b/internal/games/holdem/train.go @@ -0,0 +1,428 @@ +package holdem + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "math/rand/v2" + "sync" + + "pete/internal/games/cards" +) + +// The trainer. +// +// This is counterfactual regret minimisation, and what it produces is policy.gob +// — the table the bots read at the table. It is not on any request path; it runs +// from cmd/holdem-train, for half an hour, and then it is a file. +// +// The one thing worth understanding about it: **it plays the real game.** Every +// move it explores goes through Step, which is the same reducer the felt calls, +// so the blinds, the min-raise, the street completion and the money are the ones +// a player will actually meet. Its info-set key comes out of State.spot, which is +// the same function the bots look themselves up with. +// +// That is not tidiness, it is the whole lesson of the policy this replaces. That +// one was trained against a hand-written model of poker sitting beside the real +// engine — a model where a call always ended the street, the big blind had no +// option, and the payoff was half the pot no matter who had put what in. Then it +// was looked up under a key the trainer never wrote. The result was a 3.4MB file +// that had never once been read, and nobody could tell, because a policy miss is +// not an error. It just quietly isn't there. +// +// So: one engine, one key function, and a test that fails if the bots stop +// finding themselves in the table. + +// How much of the game tree to explore. Two raises a street keeps the tree small +// enough to converge; a third barely changes how anybody plays and multiplies the +// nodes. +const ( + maxRaisesPerStreet = 2 + maxDepth = 40 + trainMCIters = 60 // noisy, but it is only picking a bucket +) + +// regrets is what CFR accumulates: how much better each action would have been. +type regrets map[string]*[numActions]float64 + +// Trained is the file the bots read. +type Trained struct { + Strategy map[string][numActions]float64 + Meta TrainMeta +} + +// TrainMeta is what the policy can say about itself. Worth having: a policy is +// otherwise an opaque three megabytes and there is no way to tell a good one from +// a stale one by looking. +type TrainMeta struct { + Iterations int + Stakes string + Depths string + Nodes int +} + +// ---- preflop, measured once ------------------------------------------------ + +var ( + preflopOnce sync.Once + preflopTable [13][13]Equity // [hi][lo] offsuit, [lo][hi] suited, diagonal pairs +) + +// preflopEquity is the equity of a starting hand heads-up. There are only 169 +// hands that differ from each other, so they are measured properly, once, and +// then it is a lookup — which matters twice: it takes the noise out of a bucket +// boundary, and the trainer visits preflop on every single iteration. +func preflopEquity(hole [2]cards.Card) Equity { + preflopOnce.Do(func() { + rng := cards.NewRNG(20260714, 1) + for a := cards.Ace; a <= cards.King; a++ { + for b := a; b <= cards.King; b++ { + lo, hi := rankIdx(a), rankIdx(b) + + // Suited, and the pairs (which can only be offsuit) on the diagonal. + s1 := cards.Card{Rank: a, Suit: cards.Spades} + s2 := cards.Card{Rank: b, Suit: cards.Spades} + if a == b { + s2.Suit = cards.Hearts + } + preflopTable[lo][hi] = equityOf([2]cards.Card{s1, s2}, nil, 1, 10000, rng) + + if a != b { + o2 := cards.Card{Rank: b, Suit: cards.Hearts} + preflopTable[hi][lo] = equityOf([2]cards.Card{s1, o2}, nil, 1, 10000, rng) + } + } + } + }) + + lo, hi := rankIdx(hole[0].Rank), rankIdx(hole[1].Rank) + if lo > hi { + lo, hi = hi, lo + } + if hole[0].Suit == hole[1].Suit { + return preflopTable[lo][hi] // suited, and the pairs sit here too + } + if lo == hi { + return preflopTable[lo][hi] + } + return preflopTable[hi][lo] // offsuit +} + +// rankIdx maps a rank to 0–12, with the ace high — which is what it is, before +// the flop. +func rankIdx(r cards.Rank) int { + if r == cards.Ace { + return 12 + } + return int(r) - 2 +} + +// ---- the traversal --------------------------------------------------------- + +// Train runs external-sampling MCCFR for n hands and returns the average +// strategy. Each worker keeps its own tables and they are summed at the end, +// which is what makes this embarrassingly parallel and is the only reason it +// finishes in half an hour. +func Train(n, workers int, t Tier, minBB, maxBB int64, seed uint64, progress func(done int)) *Trained { + if workers < 1 { + workers = 1 + } + + type table struct { + reg regrets + avg regrets + } + out := make([]table, workers) + + var wg sync.WaitGroup + var done sync.Mutex + completed := 0 + + for w := 0; w < workers; w++ { + wg.Add(1) + go func(w int) { + defer wg.Done() + tr := &trainer{ + reg: regrets{}, + avg: regrets{}, + tier: t, + minBB: minBB, + maxBB: maxBB, + rng: cards.NewRNG(seed, uint64(w)+1), + } + + share := n / workers + if w < n%workers { + share++ + } + for i := 0; i < share; i++ { + tr.iterate(uint64(w)<<40 | uint64(i)) + if progress != nil && i%2000 == 0 { + done.Lock() + completed += 2000 + c := completed + done.Unlock() + progress(c) + } + } + out[w] = table{tr.reg, tr.avg} + }(w) + } + wg.Wait() + + // Sum the workers' average-strategy tables, then normalise each node into the + // probabilities a bot will actually play. + total := regrets{} + for _, tab := range out { + for key, v := range tab.avg { + acc, ok := total[key] + if !ok { + acc = &[numActions]float64{} + total[key] = acc + } + for i, x := range v { + acc[i] += x + } + } + } + + strategy := make(map[string][numActions]float64, len(total)) + for key, v := range total { + var sum float64 + for _, x := range v { + sum += x + } + var probs [numActions]float64 + if sum > 0 { + for i, x := range v { + probs[i] = x / sum + } + } else { + for i := range probs { + probs[i] = 1.0 / numActions + } + } + strategy[key] = probs + } + + return &Trained{ + Strategy: strategy, + Meta: TrainMeta{ + Iterations: n, + Stakes: fmt.Sprintf("%d/%d", t.SB, t.BB), + Depths: fmt.Sprintf("%d–%d BB", minBB, maxBB), + Nodes: len(strategy), + }, + } +} + +type trainer struct { + reg regrets + avg regrets + tier Tier + minBB int64 + maxBB int64 + rng *rand.Rand + + // A hand's equity on a given street depends on the cards and nothing else — + // not on how the betting went to get there. The deck is fixed for the whole + // iteration, so the flop is the same flop down every branch, and this is + // measured once per seat per street instead of once per node. + eq [2][4]Equity + have [2][4]bool +} + +// iterate deals one hand and walks it once for each player. +// +// The stack depth is drawn fresh every hand, across the whole range the table +// allows. This is the fix for the policy that came before: it was trained at ten +// big blinds and nothing else, so four out of five spots in a real cash game fell +// outside anything it had ever seen. A hand of poker is a different game at 20 +// big blinds than at 100 — that is most of what makes it a game — and the bots +// have to have played both. +func (tr *trainer) iterate(id uint64) { + depth := tr.minBB + if tr.maxBB > tr.minBB { + depth += tr.rng.Int64N(tr.maxBB - tr.minBB + 1) + } + stack := depth * tr.tier.BB + + // No rake while learning. The bots should learn to play poker, not to beat a + // fee, and the fee is the house's business. + t := tr.tier + t.RakePct = 0 + + s, err := Open(t, stack, stack, id, tr.rng.Uint64()) + if err != nil { + return + } + start := [2]int64{s.Seats[0].Stack + s.Seats[0].Bet, s.Seats[1].Stack + s.Seats[1].Bet} + + tr.have = [2][4]bool{} // one deal, one set of boards, one set of equities + + for me := 0; me < 2; me++ { + tr.walk(s.Clone(), me, start, 0) + } +} + +// equity is the cached measurement for this seat on this street. +func (tr *trainer) equity(s State, seat int) Equity { + st := s.Street + if st > River { + st = River + } + if !tr.have[seat][st] { + tr.eq[seat][st] = s.equityFor(seat, trainMCIters, tr.rng) + tr.have[seat][st] = true + } + return tr.eq[seat][st] +} + +// walk returns what the hand is worth to `me`, in chips, from here. +func (tr *trainer) walk(s State, me int, start [2]int64, depth int) float64 { + if s.Phase != PhaseBetting || depth > maxDepth { + // The hand is over (or we have gone far enough to call it over). What it was + // worth is simply what the player has now against what they sat down with — + // the real number, out of the real engine, side pots and all. + return float64(s.Seats[me].Stack - start[me]) + } + + seat := s.ToAct + key := s.spotKey(seat, tr.equity(s, seat)) + + mask := s.mask(seat) + if raises(s.History) >= maxRaisesPerStreet { + mask[actRaiseHalf], mask[actRaisePot] = false, false + } + + reg := tr.reg[key] + if reg == nil { + reg = &[numActions]float64{} + tr.reg[key] = reg + } + strat := match(*reg, mask) + + // The opponent's turn: sample one line and follow it. That is the "external + // sampling" part, and it is what keeps a hand from costing 5^12 traversals. + if seat != me { + avg := tr.avg[key] + if avg == nil { + avg = &[numActions]float64{} + tr.avg[key] = avg + } + for i, p := range strat { + avg[i] += p + } + return tr.walk(tr.play(s, seat, sample(strat, tr.rng)), me, start, depth+1) + } + + // Our turn: try everything, and regret what we didn't do. + var values [numActions]float64 + var node float64 + for a := 0; a < numActions; a++ { + if !mask[a] { + continue + } + values[a] = tr.walk(tr.play(s, seat, a), me, start, depth+1) + node += strat[a] * values[a] + } + for a := 0; a < numActions; a++ { + if mask[a] { + reg[a] += values[a] - node + } + } + return node +} + +// play applies one abstract action through the real reducer. +func (tr *trainer) play(s State, seat, action int) State { + next, _, err := Step(s.Clone(), s.moveFor(action, seat)) + if err != nil { + // The mask and the rules disagreed, which is a bug in one of them. Fold and + // carry on rather than poison the whole run. + next, _, err = Step(s.Clone(), Move{Kind: Fold}) + if err != nil { + return s + } + } + return next +} + +// match is regret matching: play each action in proportion to how much you wish +// you had played it. An action nobody regrets not taking gets played uniformly. +func match(reg [numActions]float64, mask [numActions]bool) [numActions]float64 { + var strat [numActions]float64 + var sum float64 + for i, r := range reg { + if mask[i] && r > 0 { + sum += r + } + } + if sum > 0 { + for i, r := range reg { + if mask[i] && r > 0 { + strat[i] = r / sum + } + } + return strat + } + + n := 0 + for _, ok := range mask { + if ok { + n++ + } + } + if n == 0 { + strat[actCallCheck] = 1 + return strat + } + for i, ok := range mask { + if ok { + strat[i] = 1 / float64(n) + } + } + return strat +} + +func sample(strat [numActions]float64, rng *rand.Rand) int { + r := rng.Float64() + var sum float64 + for i, p := range strat { + sum += p + if r < sum { + return i + } + } + return actCallCheck +} + +// raises counts the bets and raises on this street, which is what the tree is +// capped on. +func raises(history string) int { + n := 0 + for _, c := range history { + if c == 'r' || c == 'R' { + n++ + } + } + return n +} + +// ---- the file -------------------------------------------------------------- + +// Save writes a trained policy. +func Save(w io.Writer, t *Trained) error { return gob.NewEncoder(w).Encode(t) } + +// Load reads one. It is only used by the tests — the bots read the embedded copy. +func Load(r io.Reader) (*Trained, error) { + var t Trained + if err := gob.NewDecoder(r).Decode(&t); err != nil { + return nil, err + } + return &t, nil +} + +// loadTrained decodes the embedded policy in the new format. +func loadTrained(b []byte) (*Trained, error) { return Load(bytes.NewReader(b)) } diff --git a/internal/web/games_holdem.go b/internal/web/games_holdem.go new file mode 100644 index 0000000..fa28f6b --- /dev/null +++ b/internal/web/games_holdem.go @@ -0,0 +1,347 @@ +package web + +import ( + "encoding/json" + "errors" + "log/slog" + "net/http" + + "pete/internal/games/blackjack" + "pete/internal/games/holdem" + "pete/internal/storage" +) + +// Texas hold'em, played for chips against the trained bots. +// +// This is the only table in the casino that is a *session* rather than a game. +// Everywhere else you stake, you play once, and a multiple pays: the hand is the +// unit and the money moves at both ends of it. Poker is not that shape. You buy +// chips onto the table, you play as many hands as you feel like, and you leave +// with whatever is in front of you — so the live row lives across hands, and the +// chips move exactly twice: once when you sit down, once when you get up. +// +// Which means there is no "payout" until you stand up, and `commit` is only ever +// told the game is Done when you do (or when you have nothing left to sit with). +// In between, every pot won and lost is inside the engine, and storage sees none +// of it. That is the honest model, and it is also the safe one: a hand that dies +// halfway leaves the chips where they were, on the table, in the live row. +// +// What the browser is allowed to see: your two cards, the board, everybody's +// stacks and bets, and nothing else. Not the deck, and not a bot's hand — until +// a showdown turns it over, which is the moment it stops being a secret. + +// holdemSeatView is one seat. Cards are present only when the viewer is entitled +// to them: yours always, a bot's never, until the hand is shown down. +type holdemSeatView struct { + Name string `json:"name"` + Bot bool `json:"bot"` + You bool `json:"you"` + Stack int64 `json:"stack"` + Bet int64 `json:"bet"` + State string `json:"state"` // active | folded | allin | out + Pos string `json:"pos"` // BTN, SB, BB, UTG… + Cards []cardView `json:"cards,omitempty"` + Won int64 `json:"won,omitempty"` +} + +var seatStates = map[holdem.SeatState]string{ + holdem.Active: "active", + holdem.Folded: "folded", + holdem.AllIn: "allin", + holdem.Out: "out", +} + +// holdemView is the table as its player may see it. +type holdemView struct { + Tier holdem.Tier `json:"tier"` + Seats []holdemSeatView `json:"seats"` + Button int `json:"button"` + HandNo int `json:"hand_no"` + + Board []cardView `json:"board"` + Street string `json:"street"` + Pot int64 `json:"pot"` + Side []int64 `json:"side,omitempty"` + + ToAct int `json:"to_act"` + Phase string `json:"phase"` + + // What you may do, decided here rather than in the browser — the felt should + // never offer a button the table would refuse. + Owed int64 `json:"owed"` + CanCheck bool `json:"can_check"` + CanRaise bool `json:"can_raise"` + MinRaise int64 `json:"min_raise_to"` + MaxRaise int64 `json:"max_raise_to"` + + Stack int64 `json:"stack"` // what's in front of you + BoughtIn int64 `json:"bought_in"` + Rake int64 `json:"rake"` // what the house has taken this session + MaxTopUp int64 `json:"max_topup"` + Payout int64 `json:"payout,omitempty"` +} + +func viewHoldem(g holdem.State) holdemView { + v := holdemView{ + Tier: g.Tier, + Button: g.Button, + HandNo: g.HandNo, + Street: g.Street.String(), + Pot: g.Total(), + ToAct: g.ToAct, + Phase: string(g.Phase), + Stack: g.Seats[holdem.You].Stack, + BoughtIn: g.BoughtIn, + Rake: g.Rake, + Payout: g.Payout, + } + for _, p := range g.Side { + v.Side = append(v.Side, p.Amount) + } + // An empty board is an empty board, not null. A Go slice with nothing in it + // marshals to null, and a browser that has to write `(board || [])` everywhere + // is a browser one forgotten guard away from a crash on every preflop. + v.Board = []cardView{} + for _, c := range g.Community { + v.Board = append(v.Board, viewCard(c)) + } + + // The wall. A bot's hand crosses the wire in exactly one situation — the hand + // was shown down and they did not fold — because that is the only situation in + // which a player at a real table would be looking at it. + shown := g.Street == holdem.Showdown + for i, p := range g.Seats { + seat := holdemSeatView{ + Name: p.Name, + Bot: p.Bot, + You: i == holdem.You, + Stack: p.Stack, + Bet: p.Bet, + State: seatStates[p.State], + Pos: g.Position(i), + Won: p.Won, + } + mine := i == holdem.You + dealt := p.State != holdem.Out && p.Hole[0].Rank != 0 + if dealt && (mine || (shown && p.State != holdem.Folded)) { + seat.Cards = []cardView{viewCard(p.Hole[0]), viewCard(p.Hole[1])} + } + v.Seats = append(v.Seats, seat) + } + + if g.Phase == holdem.PhaseBetting && g.ToAct == holdem.You { + v.Owed = g.Owed(holdem.You) + v.CanCheck = v.Owed == 0 + v.CanRaise = g.CanRaise(holdem.You) + v.MinRaise = g.MinRaiseTo(holdem.You) + v.MaxRaise = g.MaxRaiseTo(holdem.You) + } + if top := g.Tier.MaxBuy - g.Seats[holdem.You].Stack; top > 0 { + v.MaxTopUp = top + } + return v +} + +// holdemEventView is one beat of the script the felt plays back. The engine only +// ever attaches a bot's cards to a showdown; this drops them again anywhere else, +// which is the second of the two walls. +type holdemEventView struct { + Kind string `json:"kind"` + Seat int `json:"seat"` + Cards []cardView `json:"cards,omitempty"` + Amount int64 `json:"amount,omitempty"` + Total int64 `json:"total,omitempty"` + Text string `json:"text,omitempty"` +} + +func viewHoldemEvents(evs []holdem.Event, g holdem.State) []holdemEventView { + out := make([]holdemEventView, 0, len(evs)) + for _, e := range evs { + v := holdemEventView{Kind: e.Kind, Seat: e.Seat, Amount: e.Amount, Total: e.Total, Text: e.Text} + for _, c := range e.Cards { + v.Cards = append(v.Cards, viewCard(c)) + } + // A card may ride an event only if it is the board, your own hand, or a hand + // being shown down. Anything else is a bot's business. + if len(v.Cards) > 0 && e.Seat >= 0 && e.Seat != holdem.You && e.Kind != "show" { + v.Cards = nil + } + out = append(out, v) + } + return out +} + +// handleHoldemSit buys chips onto a table. The chips are staked first, in the +// same statement that checks they exist, so two sit-downs fired at once cannot +// buy in with the same chip. +func (s *Server) handleHoldemSit(w http.ResponseWriter, r *http.Request) { + user, ok := s.player(w, r) + if !ok { + return + } + var req struct { + Tier string `json:"tier"` + Bots int `json:"bots"` + BuyIn int64 `json:"buyin"` + } + if err := decodeJSON(r, &req); err != nil { + http.Error(w, "bad json", http.StatusBadRequest) + return + } + tier, err := holdem.TierBySlug(req.Tier) + if err != nil { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick a table"}) + return + } + if req.BuyIn < tier.MinBuy || req.BuyIn > tier.MaxBuy { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{ + "error": "that isn't a legal buy-in for this table", + }) + return + } + if req.Bots < 1 || req.Bots > holdem.MaxBots { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick some opponents"}) + return + } + + if err := storage.Stake(user, req.BuyIn); err != nil { + if errors.Is(err, storage.ErrInsufficientChips) || errors.Is(err, storage.ErrBadAmount) { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "not enough chips to sit down"}) + return + } + slog.Error("games: holdem buy-in", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + seed1, seed2 := newSeeds() + g, evs, err := holdem.New(tier, req.Bots, req.BuyIn, blackjack.DefaultRules().RakePct, seed1, seed2) + if err != nil { + _ = storage.Award(user, req.BuyIn) // nobody sat down, so nothing was bought + slog.Error("games: holdem sit", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + s.persistHoldem(w, user, g, evs, seed1, seed2, true) +} + +// handleHoldemMove plays one move: an action in a hand, or the three that are +// about the session — deal the next hand, put more chips on the table, get up. +func (s *Server) handleHoldemMove(w http.ResponseWriter, r *http.Request) { + user, ok := s.player(w, r) + if !ok { + return + } + var move holdem.Move + if err := decodeJSON(r, &move); err != nil { + http.Error(w, "bad json", http.StatusBadRequest) + return + } + + live, err := storage.LoadLiveHand(user) + if errors.Is(err, storage.ErrNoLiveHand) { + writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "you're not at a table"}) + return + } + if err != nil { + slog.Error("games: holdem load", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if live.Game != gameHoldem { + writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "finish the game you're in first"}) + return + } + var g holdem.State + if err := json.Unmarshal(live.State, &g); err != nil { + slog.Error("games: unreadable holdem table", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + // A top-up is real money crossing the border, so the chips come off the stack + // before the engine is asked — and go straight back if it says no. Same order, + // and the same reason, as doubling down at blackjack. + topped := int64(0) + if move.Kind == holdem.TopUp { + if err := storage.Stake(user, move.Amount); err != nil { + if errors.Is(err, storage.ErrInsufficientChips) || errors.Is(err, storage.ErrBadAmount) { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "you don't have those chips"}) + return + } + slog.Error("games: holdem top-up", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + topped = move.Amount + } + + next, evs, err := holdem.ApplyMove(g, move) + if err != nil { + if topped > 0 { + _ = storage.Award(user, topped) // the top-up didn't happen + } + msg := "that move isn't legal here" + switch { + case errors.Is(err, holdem.ErrHandLive): + msg = "finish the hand first" + case errors.Is(err, holdem.ErrNotYourTurn): + msg = "it isn't your turn" + case errors.Is(err, holdem.ErrCantCheck): + msg = "there's a bet to you" + case errors.Is(err, holdem.ErrTooSmall): + msg = "that's under the minimum raise" + case errors.Is(err, holdem.ErrTooBig): + msg = "you don't have that many chips" + case errors.Is(err, holdem.ErrBadBuyIn): + msg = "that would put you over the table maximum" + } + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg}) + return + } + s.persistHoldem(w, user, next, evs, live.Seed1, live.Seed2, false) +} + +// persistHoldem writes the table back and answers the browser. +// +// The session settles exactly once — when the player gets up, or when they have +// nothing left to get up with. Until then Done is false and `commit` moves no +// chips at all, which is what makes a hundred hands of poker a single trip across +// the border rather than a hundred. +func (s *Server) persistHoldem(w http.ResponseWriter, user string, g holdem.State, evs []holdem.Event, seed1, seed2 uint64, fresh bool) { + blob, err := json.Marshal(g) + if err != nil { + slog.Error("games: marshal holdem", "user", user, "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + done := g.Phase == holdem.PhaseDone + outcome := "left" + switch { + case done && g.Payout == 0: + outcome = "busted" + case done && g.Payout > g.BoughtIn: + outcome = "up" + case done && g.Payout < g.BoughtIn: + outcome = "down" + } + + v, ok := s.commit(w, user, finished{ + Game: gameHoldem, Blob: blob, + Bet: g.BoughtIn, Payout: g.Payout, Rake: g.Rake, + Outcome: outcome, Done: done, + Seed1: seed1, Seed2: seed2, Fresh: fresh, + }) + if !ok { + return + } + // A closed session is gone from storage, so the table view has none to show — + // but the browser still needs the last board to land the verdict on. + if done { + hv := viewHoldem(g) + v.Holdem = &hv + } + v.HoldemEvents = viewHoldemEvents(evs, g) + writeJSON(w, v) +} diff --git a/internal/web/games_holdem_test.go b/internal/web/games_holdem_test.go new file mode 100644 index 0000000..0df423e --- /dev/null +++ b/internal/web/games_holdem_test.go @@ -0,0 +1,239 @@ +package web + +import ( + "testing" + + "pete/internal/games/holdem" + "pete/internal/storage" +) + +// Sitting down is the only time chips leave your stack at this table, and getting +// up is the only time they come back. Everything in between is inside the engine. +func TestHoldemSitTakesTheBuyInAndNothingElse(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + v, code := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "low", "bots": 2, "buyin": 600})) + if code != 200 { + t.Fatalf("sit = %d, want 200", code) + } + if v.Chips != 4400 { + t.Fatalf("chips after a 600 buy-in = %d, want 4400", v.Chips) + } + if v.Game != gameHoldem || v.Holdem == nil { + t.Fatalf("sit returned no table: game=%q", v.Game) + } + + g := v.Holdem + if g.Stack != 600 { + t.Errorf("you sat down with %d, want the 600 you bought in for", g.Stack) + } + if len(g.Seats) != 3 { + t.Fatalf("two bots and you is three seats, got %d", len(g.Seats)) + } + if g.Phase != "handover" { + t.Errorf("phase %q — a table you just sat at has no hand on it yet", g.Phase) + } + + // Play a whole hand, then check that not one chip has crossed the border. + deal, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "deal"})) + if code != 200 { + t.Fatalf("deal = %d, want 200", code) + } + for i := 0; deal.Holdem != nil && deal.Holdem.Phase == "betting" && i < 60; i++ { + move := "check" + if deal.Holdem.Owed > 0 { + move = "fold" + } + deal, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": move})) + if code != 200 { + t.Fatalf("%s = %d, want 200", move, code) + } + } + if deal.Chips != 4400 { + t.Errorf("chips moved during a hand: %d, want the 4400 that were left after the buy-in — "+ + "a pot is settled inside the engine, not across the border", deal.Chips) + } +} + +// The wall. A bot's hole cards are the game; a browser that held them would make +// this unplayable, and the payload is where that has to be true. +func TestHoldemNeverSendsABotsCards(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + v, _ := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "micro", "bots": 3, "buyin": 200})) + if v.Holdem == nil { + t.Fatal("no table") + } + + // Play hands until one of them ends without a showdown, checking every payload + // on the way. Folding is the case that matters: nobody has earned the right to + // see anybody's cards, so nobody's may be in there. + for hand := 0; hand < 8; hand++ { + v, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "deal"})) + if code != 200 { + t.Fatalf("deal = %d", code) + } + noBotCards(t, v) + + for i := 0; v.Holdem != nil && v.Holdem.Phase == "betting" && i < 60; i++ { + move := "check" + if v.Holdem.Owed > 0 { + move = "call" + } + v, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": move})) + if code != 200 { + t.Fatalf("%s = %d", move, code) + } + noBotCards(t, v) + } + if v.Holdem == nil || v.Holdem.Phase == "done" { + return // busted out; the wall held all the way + } + } +} + +// noBotCards checks a payload. A bot's cards may appear in exactly one place: a +// seat that is being shown down, on a board that reached a showdown. +func noBotCards(t *testing.T, v tableView) { + t.Helper() + g := v.Holdem + if g == nil { + return + } + shown := g.Street == "showdown" + for i, seat := range g.Seats { + if i == 0 || len(seat.Cards) == 0 { + continue + } + if !shown { + t.Fatalf("seat %d (%s) sent %d cards on the %s — nobody has shown down", + i, seat.Name, len(seat.Cards), g.Street) + } + if seat.State == "folded" { + t.Fatalf("seat %d (%s) folded and its cards were sent anyway", i, seat.Name) + } + } + for _, e := range v.HoldemEvents { + if e.Seat > 0 && len(e.Cards) > 0 && e.Kind != "show" { + t.Fatalf("a %q event carries seat %d's cards — that's a bot's hand", e.Kind, e.Seat) + } + } +} + +// Getting up is what pays. Everything the session did lands in one movement. +func TestHoldemLeavingBringsTheStackBack(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + v, _ := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "low", "bots": 1, "buyin": 500})) + if v.Chips != 4500 || v.Holdem == nil { + t.Fatalf("sit: chips=%d holdem=%v", v.Chips, v.Holdem) + } + stack := v.Holdem.Stack + + v, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "leave"})) + if code != 200 { + t.Fatalf("leave = %d, want 200", code) + } + if v.Chips != 4500+stack { + t.Errorf("chips after getting up = %d, want %d (the %d that was in front of us)", + v.Chips, 4500+stack, stack) + } + if v.Game != "" { + t.Errorf("still at a table after getting up: %q", v.Game) + } + + // And there is no table left to play at. + _, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "deal"})) + if code != 409 { + t.Errorf("dealt a hand at a table we got up from: %d, want 409", code) + } +} + +// You cannot walk out on a hand you have chips riding on. +func TestHoldemCannotLeaveMidHand(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "low", "bots": 2, "buyin": 500})) + v, _ := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "deal"})) + if v.Holdem == nil || v.Holdem.Phase != "betting" { + t.Skip("the hand ended before we could act") + } + + _, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "leave"})) + if code != 400 { + t.Errorf("left in the middle of a hand: %d, want 400", code) + } + + // And the chips are still on the table, not back on the stack. + after, err := storage.Chips("@reala:parodia.dev") + if err != nil { + t.Fatal(err) + } + if after.Chips != 4500 { + t.Errorf("chips = %d, want 4500 — the buy-in is still on the table", after.Chips) + } +} + +// A buy-in outside the table's range is not a buy-in, and it must not take chips. +func TestHoldemRefusesABadBuyIn(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + tier, _ := holdem.TierBySlug("low") + for _, amount := range []int64{tier.MinBuy - 1, tier.MaxBuy + 1, 0} { + _, code := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "low", "bots": 2, "buyin": amount})) + if code != 400 { + t.Errorf("buy-in of %d at a %d–%d table = %d, want 400", amount, tier.MinBuy, tier.MaxBuy, code) + } + } + + st, err := storage.Chips("@reala:parodia.dev") + if err != nil { + t.Fatal(err) + } + if st.Chips != 5000 { + t.Errorf("a refused buy-in took %d chips", 5000-st.Chips) + } +} + +// A top-up is real money crossing the border. If the engine refuses it, the chips +// come straight back — the same order, and the same reason, as doubling down. +func TestHoldemTopUpRefundsWhenRefused(t *testing.T) { + s := newCasino(t) + fund(t, 5000) + + // Sitting down at the maximum means there is no room to top up into. + call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit", + map[string]any{"tier": "low", "bots": 1, "buyin": 1000})) + + _, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move", + map[string]any{"move": "topup", "amount": 100})) + if code != 400 { + t.Errorf("topped up over the table maximum: %d, want 400", code) + } + + st, err := storage.Chips("@reala:parodia.dev") + if err != nil { + t.Fatal(err) + } + if st.Chips != 4000 { + t.Errorf("chips = %d, want 4000 — a refused top-up must give the chips back", st.Chips) + } +} diff --git a/internal/web/games_pages.go b/internal/web/games_pages.go index 838cf27..1745203 100644 --- a/internal/web/games_pages.go +++ b/internal/web/games_pages.go @@ -6,6 +6,7 @@ import ( "pete/internal/games/blackjack" "pete/internal/games/hangman" + "pete/internal/games/holdem" "pete/internal/games/klondike" "pete/internal/games/trivia" "pete/internal/games/uno" @@ -28,9 +29,10 @@ type gameTeaser struct { Blurb string } -var comingSoon = []gameTeaser{ - {Name: "Hold'em", Emoji: "♠️", Blurb: "Six seats, and the house bots know how to play."}, -} +// comingSoon is empty, and that is the point: every game the plan named is now on +// the felt. Leave it here — the lobby renders nothing for an empty list, and the +// next game to be dreamed up goes in it. +var comingSoon = []gameTeaser{} // betDenominations are the chips you build a bet out of. var betDenominations = []int64{5, 25, 100, 500} @@ -78,6 +80,8 @@ type gamesPage struct { Quizzes []trivia.Tier // trivia's three difficulties Rungs int // how long the trivia ladder is Tables []uno.Tier // uno's three tables, and how many bots sit at each + Stakes []holdem.Tier // hold'em's three tables, by blinds + MaxBots int // how many seats hold'em will fill with bots } // casinoRoutes hangs every table off the mux. @@ -93,6 +97,7 @@ func (s *Server) casinoRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /games/solitaire", s.handleSolitaire) mux.HandleFunc("GET /games/trivia", s.handleTrivia) mux.HandleFunc("GET /games/uno", s.handleUno) + mux.HandleFunc("GET /games/holdem", s.handleHoldem) mux.HandleFunc("GET /api/games/table", s.handleTable) mux.HandleFunc("POST /api/games/buyin", s.handleBuyIn) @@ -112,6 +117,9 @@ func (s *Server) casinoRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /api/games/uno/start", s.handleUnoStart) mux.HandleFunc("POST /api/games/uno/move", s.handleUnoMove) + + mux.HandleFunc("POST /api/games/holdem/sit", s.handleHoldemSit) + mux.HandleFunc("POST /api/games/holdem/move", s.handleHoldemMove) } // requirePlayer sends an anonymous visitor to sign in and comes back here after. @@ -145,6 +153,8 @@ func (s *Server) gamesPage(r *http.Request) gamesPage { Quizzes: trivia.Tiers, Rungs: trivia.Rungs, Tables: uno.Tiers, + Stakes: holdem.Tiers, + MaxBots: holdem.MaxBots, } } @@ -189,3 +199,10 @@ func (s *Server) handleUno(w http.ResponseWriter, r *http.Request) { } s.render(w, "uno", s.gamesPage(r)) } + +func (s *Server) handleHoldem(w http.ResponseWriter, r *http.Request) { + if !s.requirePlayer(w, r) { + return + } + s.render(w, "holdem", s.gamesPage(r)) +} diff --git a/internal/web/games_play.go b/internal/web/games_play.go index 1f7987e..abeeb0e 100644 --- a/internal/web/games_play.go +++ b/internal/web/games_play.go @@ -13,6 +13,7 @@ import ( "pete/internal/games/blackjack" "pete/internal/games/cards" "pete/internal/games/hangman" + "pete/internal/games/holdem" "pete/internal/games/klondike" "pete/internal/games/trivia" "pete/internal/games/uno" @@ -197,6 +198,9 @@ type tableView struct { Uno *unoView `json:"uno,omitempty"` UnoEvents []unoEventView `json:"uno_events,omitempty"` + Holdem *holdemView `json:"holdem,omitempty"` + HoldemEvents []holdemEventView `json:"holdem_events,omitempty"` + Rake float64 `json:"rake_pct"` } @@ -264,6 +268,13 @@ func (s *Server) table(user string) (tableView, error) { } uv := viewUno(g) v.Uno = &uv + case gameHoldem: + var g holdem.State + if err := json.Unmarshal(live.State, &g); err != nil { + return s.dropUnreadable(user, v, err) + } + hv := viewHoldem(g) + v.Holdem = &hv default: return s.dropUnreadable(user, v, fmt.Errorf("unknown game %q", live.Game)) } @@ -521,6 +532,7 @@ const ( gameSolitaire = "solitaire" gameTrivia = "trivia" gameUno = "uno" + gameHoldem = "holdem" ) // finished is what commit needs to know about a game it's writing back: enough diff --git a/internal/web/games_render_test.go b/internal/web/games_render_test.go index d33a9c4..85ff60c 100644 --- a/internal/web/games_render_test.go +++ b/internal/web/games_render_test.go @@ -23,6 +23,7 @@ func TestEveryCasinoPageRenders(t *testing.T) { "/games/solitaire", "/games/trivia", "/games/uno", + "/games/holdem", } mux := http.NewServeMux() diff --git a/internal/web/server.go b/internal/web/server.go index d3c0f5a..4d55626 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -95,7 +95,7 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo pages []string }{ {"layout", []string{"_card"}, []string{"index", "channel", "weather", "bookmarks", "for-you", "status", "story"}}, - {"games_layout", []string{"_chipbar"}, []string{"games", "blackjack", "hangman", "solitaire", "trivia", "uno"}}, + {"games_layout", []string{"_chipbar"}, []string{"games", "blackjack", "hangman", "solitaire", "trivia", "uno", "holdem"}}, } tpls := make(map[string]*template.Template) for _, set := range sets { diff --git a/internal/web/static/css/input.css b/internal/web/static/css/input.css index 54d6a31..747a6a9 100644 --- a/internal/web/static/css/input.css +++ b/internal/web/static/css/input.css @@ -1894,3 +1894,257 @@ html[data-room] .pete-felt { .cs-stack[data-chip="25"] { --chip: #4caf7d; } .cs-stack[data-chip="100"] { --chip: #2b2118; } .cs-stack[data-chip="500"] { --chip: #b079d6; } + +/* ── Hold'em ──────────────────────────────────────────────────────────────── + The one table with other people at it. Everything else in the casino has a + single bet spot, because there was only ever one player; here every seat has + its own, chips travel between them and the pot rather than between you and the + house, and the pot in the middle is the thing they all move toward. + + Seats are a wrapping row rather than an ellipse. An oval of six seats is what a + poker table looks like, and it is also what stops fitting the moment a phone is + held up — this keeps the geometry honest at 390px and still reads as a table + because the board and the pot sit in the middle of it, which is where the eye + goes anyway. */ + +.pete-poker { --seat-w: 7.5rem; } + +.pete-poker-seats { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: center; + gap: 0.75rem; +} + +.pete-seat { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + width: var(--seat-w); + transition: opacity 0.3s ease, filter 0.3s ease; +} +/* A folded seat is still there — you can see they're in the game and out of the + hand, which is information you're entitled to. It just stops competing. */ +.pete-seat[data-state="folded"] { opacity: 0.38; filter: grayscale(0.7); } +.pete-seat[data-state="out"] { opacity: 0.2; } + +/* Whose turn it is. The ring is the only thing on the felt that moves on its own, + because it is the only thing you are waiting for. */ +.pete-seat[data-turn="1"] .pete-seat-plate { + box-shadow: 0 0 0 2px var(--accent), 0 0 1.6rem -0.2rem var(--accent); + animation: pete-seat-wait 1.6s ease-in-out infinite; +} +@keyframes pete-seat-wait { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-2px); } +} + +.pete-seat-cards { + display: flex; + justify-content: center; + gap: 0.15rem; + min-height: 4.4rem; + --card-h: 4.4rem; + --card-w: 3.15rem; +} +/* A seat that folded throws its cards in. */ +.pete-seat-cards[data-mucked="1"] { opacity: 0; transform: translateY(-0.6rem) scale(0.9); transition: all 0.35s ease; } + +.pete-seat-plate { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 0.3rem 0.4rem; + border-radius: 0.85rem; + background: rgba(0,0,0,0.32); + border: 1px solid rgba(255,255,255,0.12); + transition: box-shadow 0.25s ease; +} +.pete-seat-name { + font-family: var(--font-display, inherit); + font-weight: 700; + font-size: 0.8rem; + line-height: 1.1; + color: rgba(255,255,255,0.92); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.pete-seat-stack { + font-weight: 700; + font-size: 0.78rem; + font-variant-numeric: tabular-nums; + color: rgba(255,255,255,0.58); +} +.pete-seat[data-state="allin"] .pete-seat-stack::after { + content: " all in"; + color: var(--accent); +} + +/* The dealer button, and the blinds. Small, and worth having: position is most of + what makes one hand different from the next, and a player who cannot see where + the button is cannot see the game. */ +.pete-seat-pos { + position: absolute; + top: -0.5rem; + right: -0.5rem; + display: grid; + place-items: center; + min-width: 1.45rem; + height: 1.45rem; + padding: 0 0.3rem; + border-radius: 999px; + font-size: 0.58rem; + font-weight: 800; + letter-spacing: 0.02em; + background: rgba(255,255,255,0.9); + color: #2b2118; + box-shadow: 0 2px 0 rgba(0,0,0,0.3); +} +.pete-seat-pos[data-pos="BTN"] { background: #f5d76e; } + +/* Every seat's bet, sitting between them and the pot. + A .pete-spot is 7rem across, because blackjack has exactly one of them. Six of + those is most of a felt, so a seat's is less than half the size — and so are the + chips in it, which are otherwise bigger than the ring they land in. */ +.pete-seat-spot { + height: 3.6rem; + width: 3.6rem; + margin-bottom: 0.5rem; /* the bet total hangs below the ring; leave it somewhere to hang */ +} +.pete-seat-spot .pete-disc { + height: 1.6rem; + width: 1.6rem; + margin: -0.8rem 0 0 -0.8rem; +} +.pete-seat-spot .pete-spot-total { + font-size: 0.66rem; + padding: 0.05rem 0.45rem; +} + +/* The middle of the table: the board, and the pot under it. */ +.pete-poker-middle { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + padding: 0.5rem 0; +} +/* The pot's chips stack upwards, so they need room under the board or they climb + into the river card. */ +.pete-poker-pot { margin-top: 0.9rem; } +/* The board keeps its height when it is empty, so the felt does not jump a hundred + pixels the moment the flop lands. */ +.pete-poker-board { + display: flex; + gap: 0.35rem; + min-height: 6rem; + --card-h: 6rem; + --card-w: 4.3rem; +} +.pete-poker-pot { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1rem; +} +/* The pot's chips need a box to themselves. .pete-stack is position:absolute with + inset:0, so a pile that shares a box with the number under it lands on top of + the number — which is exactly what it did: the pot showed a chip and no total. */ +.pete-poker-pot-pile { + position: relative; + width: 7rem; + height: 3rem; +} +.pete-poker-pot-label { + font-size: 0.62rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(255,255,255,0.42); +} +.pete-poker-pot-total { + font-family: var(--font-display, inherit); + font-size: 1.5rem; + font-weight: 800; + font-variant-numeric: tabular-nums; + color: #fff; + text-shadow: 0 2px 0 rgba(0,0,0,0.28); +} +.pete-poker-side { + font-size: 0.68rem; + font-weight: 700; + color: rgba(255,255,255,0.5); +} + +/* Your seat. Bigger cards, because they are the two you are actually reading. */ +.pete-poker-you .pete-seat { width: auto; } +.pete-poker-you .pete-seat-cards { + --card-h: 6.8rem; + --card-w: 4.85rem; + gap: 0.3rem; + min-height: 6.8rem; +} +.pete-poker-you .pete-seat-plate { padding: 0.4rem 1.1rem; } +.pete-poker-you .pete-seat-name { font-size: 0.95rem; } +.pete-poker-you .pete-seat-stack { font-size: 0.95rem; } + +/* What the hand did to you, said once, in the middle of the felt. */ +.pete-poker-verdict { + border-radius: 999px; + background: rgba(255,255,255,0.95); + padding: 0.5rem 1.25rem; + font-family: var(--font-display, inherit); + font-size: 1.05rem; + font-weight: 700; + color: #2b2118; + box-shadow: 0 4px 0 rgba(0,0,0,0.18); +} +.pete-poker-verdict[data-tone="win"] { background: #4caf7d; color: #fff; } +.pete-poker-verdict[data-tone="lose"] { background: rgba(255,255,255,0.9); color: #7a5c50; } + +/* The action bar. Raise is a slider, because a raise is a *size* and a text box + makes you type a number you have not thought about. */ +.pete-raise { + display: flex; + align-items: center; + gap: 0.6rem; + flex: 1 1 14rem; + min-width: 0; +} +.pete-raise input[type="range"] { + flex: 1; + min-width: 0; + accent-color: var(--accent); +} +.pete-raise-to { + font-family: var(--font-display, inherit); + font-size: 1.15rem; + font-weight: 800; + font-variant-numeric: tabular-nums; + min-width: 3.5rem; + text-align: right; +} + +.pete-poker-log { + max-height: 7rem; + overflow-y: auto; + font-size: 0.75rem; + line-height: 1.5; + color: rgba(255,255,255,0.55); +} +.pete-poker-log b { color: rgba(255,255,255,0.85); font-weight: 700; } + +@media (max-width: 640px) { + .pete-poker { --seat-w: 5.6rem; } + .pete-seat-cards { --card-h: 3.5rem; --card-w: 2.5rem; min-height: 3.5rem; } + .pete-poker-board { --card-h: 5rem; --card-w: 3.55rem; min-height: 5rem; gap: 0.25rem; } + .pete-poker-you .pete-seat-cards { --card-h: 6rem; --card-w: 4.3rem; min-height: 6rem; } + .pete-poker-pot-total { font-size: 1.25rem; } +} diff --git a/internal/web/static/css/output.css b/internal/web/static/css/output.css index b8afa8d..6cacbfc 100644 --- a/internal/web/static/css/output.css +++ b/internal/web/static/css/output.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.65)}.pete-reader-shell{position:relative;height:100%;width:100%;max-width:44rem;margin:0 auto;display:flex;flex-direction:column;padding:max(env(safe-area-inset-top),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.35;cursor:default;transform:none}.pete-reader-btn-open{background:var(--accent);border-color:transparent;color:#1c1305;text-decoration:none}.pete-reader-btn-open:hover{background:var(--accent);filter:brightness(1.08)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.12)}.pete-reader-typebtns button.is-active{background:var(--accent);color:#1c1305;border-color:transparent}.pete-reader-voicerow[hidden]{display:none}.pete-reader-voice{font:inherit;font-size:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.08)}.pete-reader-scroll{flex:1 1 auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border-radius:1.75rem}.pete-reader-scroll:focus{outline:none}.pete-reader-article{background:var(--card);color:var(--ink);border-radius:1.75rem;border:2px solid rgba(0,0,0,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.01em;font-size:clamp(1.6rem,4.5vw,2.4rem);margin-bottom:1rem}.pete-reader-hero{width:100%;border-radius:1.1rem;margin-bottom:1.25rem;background:rgba(0,0,0,.05)}.pete-reader-scroll{--reader-fs:1.08rem}.pete-reader-body{font-size:var(--reader-fs);line-height:1.75}.pete-reader-body p{margin-bottom:1.05em}.pete-reader-body p:last-child{margin-bottom:0}.pete-reader-scroll.is-size-s{--reader-fs:0.98rem}.pete-reader-scroll.is-size-m{--reader-fs:1.08rem}.pete-reader-scroll.is-size-l{--reader-fs:1.22rem}.pete-reader-scroll.is-size-xl{--reader-fs:1.4rem}.pete-reader-scroll.is-serif .pete-reader-body{font-family:Georgia,Iowan Old Style,Times New Roman,serif}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow:0 0 0 .35rem color-mix(in srgb,var(--accent) 32%,transparent)}.pete-reader-note{margin-top:1.25rem;padding-top:1rem;border-top:1px solid rgba(0,0,0,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.12)}.pete-reader-note a{color:var(--accent);font-weight:700}.pete-reader-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.55)}.pete-disc[data-chip="5"]{--chip:#5aa9e6}.pete-disc[data-chip="25"]{--chip:#4caf7d}.pete-disc[data-chip="100"]{--chip:#2b2118}.pete-disc[data-chip="500"]{--chip:#b079d6}.pete-chip{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.pete-stack{position:absolute;inset:0;pointer-events:none}.pete-stack .pete-disc{position:absolute;left:50%;top:50%;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-fly-layer{position:fixed;inset:0;z-index:60;pointer-events:none;overflow:hidden}.pete-fly{position:absolute;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;will-change:transform}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.36rem*var(--stack, 3))}.pete-rack span[data-chip="5"]{--chip:#5aa9e6}.pete-rack span[data-chip="25"]{--chip:#4caf7d}.pete-rack span[data-chip="100"]{--chip:#2b2118}.pete-rack span[data-chip="500"]{--chip:#b079d6}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(0,0,0,.3);stroke-width:3.5}.pete-gallows-body>*{stroke:#fff;stroke-width:5;opacity:0;filter:drop-shadow(0 2px 0 rgba(0,0,0,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.97)}@keyframes pete-shake{10%,90%{transform:translateX(-1.5px)}20%,80%{transform:translateX(3px)}30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.5rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.4rem;font-weight:700;color:#fff;background:rgba(0,0,0,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.45)}.pete-meter-value{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--accent)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.6rem;background:color-mix(in srgb,var(--ink) 6%,transparent);border:2px solid color-mix(in srgb,var(--ink) 10%,transparent);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-weight:700;color:var(--ink);transition:transform .08s ease,background .15s ease,opacity .15s ease}.pete-key:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 12%,transparent)}.pete-key:active:not(:disabled){transform:translateY(1px) scale(.96)}.pete-key:disabled{cursor:default}.pete-key[data-state=hit]{background:#4caf7d;border-color:#3d9367;color:#fff;opacity:1}.pete-key[data-state=miss]{background:color-mix(in srgb,var(--ink) 4%,transparent);color:color-mix(in srgb,var(--ink) 30%,transparent);text-decoration:line-through}.pete-key:disabled[data-state=""]{opacity:.4}.pete-tier{background:color-mix(in srgb,var(--ink) 4%,transparent);border-color:color-mix(in srgb,var(--ink) 10%,transparent)}.pete-tier:hover{background:color-mix(in srgb,var(--ink) 8%,transparent)}.pete-tier[data-on="1"]{background:color-mix(in srgb,var(--accent) 18%,transparent);border-color:var(--accent)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.pete-solitaire{--card-w:clamp(2.5rem,10.6vw,5.2rem);--card-h:calc(var(--card-w)*1.4);--fan-up:calc(var(--card-h)*0.29);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);cursor:pointer}.pete-stock:hover{filter:brightness(1.08)}.pete-stock:active{transform:translateY(1px) scale(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.pete-waste{position:relative;display:flex;height:var(--card-h);min-width:var(--card-w)}.pete-waste .pete-card+.pete-card{margin-left:calc(var(--card-w)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.5rem;align-items:start;min-height:calc(var(--card-h)*2.6)}.pete-col{display:flex;flex-direction:column;align-items:center;min-height:var(--card-h)}.pete-col .pete-card{position:relative}.pete-col .pete-card[data-face=down]+.pete-card{margin-top:calc(var(--fan-down) - var(--card-h))}.pete-col .pete-card[data-face=up]+.pete-card{margin-top:calc(var(--fan-up) - var(--card-h))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.pete-rail{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem 1.5rem}@media (min-width:1024px){.pete-rail{flex-direction:column;justify-content:flex-start;width:9.5rem;gap:1.5rem}}.pete-rack[data-at=rail]{position:static;justify-content:center}.pete-uno[data-c=red]{--play:#d64545}.pete-uno[data-c=blue]{--play:#3d7fd6}.pete-uno[data-c=yellow]{--play:#e0b02c}.pete-uno[data-c=green]{--play:#46a86b}.pete-uno:before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(80% 55% at 50% 45%,var(--play,transparent),transparent 70%);opacity:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;overflow:hidden;background:var(--uno,#2b2118);color:#fff}.pete-uno-card[data-c=red]{--uno:#d64545}.pete-uno-card[data-c=blue]{--uno:#3d7fd6}.pete-uno-card[data-c=yellow]{--uno:#e0b02c}.pete-uno-card[data-c=green]{--uno:#46a86b}.pete-uno-card[data-c=wild]{--uno:#2b2118}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.pete-uno-oval{position:relative;z-index:1;display:grid;place-items:center;height:78%;width:62%;border-radius:50%;transform:rotate(-24deg);background:#fff;color:var(--uno,#2b2118);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.45rem;font-weight:700;line-height:1;text-shadow:0 1px 0 rgba(0,0,0,.1)}.pete-uno-card[data-c=wild] .pete-uno-oval{color:#2b2118;font-size:1.15rem}.pete-uno-corner{position:absolute;z-index:1;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.5rem)}.pete-uno-hand .pete-uno-card[data-on="1"]:hover{transform:translateY(-1.1rem) scale(1.03)}.pete-uno-hand .pete-uno-card[data-on="0"]{filter:brightness(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.12);transform:translateY(-2px)}.pete-uno-seat[data-uno="1"] .pete-uno-count{color:#ffd76e}.pete-uno-fan{display:flex;--uno-h:3.2rem;--uno-w:2.15rem}.pete-uno-fan .pete-uno-card{margin-left:-1.35rem;transform:rotate(calc((var(--i, 0) - (var(--n, 1) - 1)/2)*4deg));transform-origin:bottom center;box-shadow:0 2px 0 rgba(0,0,0,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{transform:none}30%{transform:translateY(-4px) rotate(-3deg)}65%{transform:translateY(2px) rotate(2deg)}}.pete-uno-discard{display:grid;place-items:center;height:6.4rem;width:4.3rem;border-radius:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(0,0,0,.3)}.pete-uno-colour[data-c=red]{background:#d64545}.pete-uno-colour[data-c=blue]{background:#3d7fd6}.pete-uno-colour[data-c=yellow]{background:#e0b02c;color:#2b2118}.pete-uno-colour[data-c=green]{background:#46a86b}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .9s ease forwards}.pete-uno-badge[data-tone=bad]{background:#cc3d4a;color:#fff}.pete-uno-badge[data-tone=uno]{background:#e0b02c}@keyframes pete-uno-badge{0%{transform:translate(-50%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.75rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1rem;font-weight:700;color:#fff;background:var(--sw,#666);box-shadow:0 3px 0 rgba(0,0,0,.3);transition:transform .12s ease,filter .12s ease}.pete-uno-swatch:hover{transform:translateY(-2px);filter:brightness(1.08)}.pete-uno-swatch:active{transform:translateY(1px)}.pete-uno-swatch[data-c=red]{--sw:#d64545}.pete-uno-swatch[data-c=blue]{--sw:#3d7fd6}.pete-uno-swatch[data-c=yellow]{--sw:#e0b02c;color:#2b2118}.pete-uno-swatch[data-c=green]{--sw:#46a86b}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-discard,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-discard{height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.bg-theme-music{background-color:#b079d6}.bg-theme-anime{background-color:#ec5e8a}.bg-theme-foss{background-color:#d97706}.bg-theme-kids{background-color:#14b8a6}.bg-theme-finance{background-color:#10b981}.bg-theme-lego{background-color:#d01012}.bg-theme-adventure{background-color:#6d4bd8}.text-theme-gaming{color:#2d8a5a}.text-theme-tech{color:#2f7fb8}.text-theme-politics{color:#b8523a}.text-theme-eu{color:#039}.text-theme-music{color:#8a4fb8}.text-theme-anime{color:#c33a6a}.text-theme-foss{color:#b8530a}.text-theme-kids{color:#0f766e}.text-theme-finance{color:#059669}.text-theme-lego{color:#b00d0e}.text-theme-adventure{color:#5836b8}.decoration-theme-gaming{text-decoration-color:#4caf7d}.decoration-theme-tech{text-decoration-color:#5aa9e6}.decoration-theme-politics{text-decoration-color:#e07a5f}.decoration-theme-eu{text-decoration-color:#039}.decoration-theme-music{text-decoration-color:#b079d6}.decoration-theme-anime{text-decoration-color:#ec5e8a}.decoration-theme-foss{text-decoration-color:#d97706}.decoration-theme-kids{text-decoration-color:#14b8a6}.decoration-theme-finance{text-decoration-color:#10b981}.decoration-theme-lego{text-decoration-color:#d01012}.decoration-theme-adventure{text-decoration-color:#6d4bd8}.border-theme-gaming{border-color:#4caf7d}.border-theme-tech{border-color:#5aa9e6}.border-theme-politics{border-color:#e07a5f}.border-theme-eu{border-color:#039}.border-theme-music{border-color:#b079d6}.border-theme-anime{border-color:#ec5e8a}.border-theme-foss{border-color:#d97706}.border-theme-kids{border-color:#14b8a6}.border-theme-finance{border-color:#10b981}.border-theme-lego{border-color:#d01012}.border-theme-adventure{border-color:#6d4bd8}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.paywall-stamp{position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;z-index:5;background:rgba(255,250,240,.18)}.paywall-stamp:before{content:"PAYWALLED";font-family:var(--font-display,ui-sans-serif),system-ui,sans-serif;font-weight:900;font-size:clamp(1rem,3.2vw,1.75rem);letter-spacing:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}html[data-phase=dawn]{--bg:#ffe7d6;--bg-grad:#ffc9c9;--card:#fff4ea;--ink:#4a2e2a;--accent:#ff8a65}html[data-phase=dusk]{--bg:#ffd6a8;--bg-grad:#f7a07e;--card:#fff1de;--ink:#3d2417;--accent:#e6553a}html[data-phase=night]{--bg:#1a1f3a;--bg-grad:#2a3358;--card:#2d365a;--ink:#f1ecd8;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,var(--felt-a) 0,var(--felt-b) 55%,var(--felt-c) 100%)}@media (prefers-reduced-motion:reduce){html[data-room=casino-night] .cs-room:before{animation:none}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.14)}.cs-stack[data-chip="5"]{--chip:#5aa9e6}.cs-stack[data-chip="25"]{--chip:#4caf7d}.cs-stack[data-chip="100"]{--chip:#2b2118}.cs-stack[data-chip="500"]{--chip:#b079d6}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.65)}.pete-reader-shell{position:relative;height:100%;width:100%;max-width:44rem;margin:0 auto;display:flex;flex-direction:column;padding:max(env(safe-area-inset-top),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.35;cursor:default;transform:none}.pete-reader-btn-open{background:var(--accent);border-color:transparent;color:#1c1305;text-decoration:none}.pete-reader-btn-open:hover{background:var(--accent);filter:brightness(1.08)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.12)}.pete-reader-typebtns button.is-active{background:var(--accent);color:#1c1305;border-color:transparent}.pete-reader-voicerow[hidden]{display:none}.pete-reader-voice{font:inherit;font-size:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.08)}.pete-reader-scroll{flex:1 1 auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border-radius:1.75rem}.pete-reader-scroll:focus{outline:none}.pete-reader-article{background:var(--card);color:var(--ink);border-radius:1.75rem;border:2px solid rgba(0,0,0,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.01em;font-size:clamp(1.6rem,4.5vw,2.4rem);margin-bottom:1rem}.pete-reader-hero{width:100%;border-radius:1.1rem;margin-bottom:1.25rem;background:rgba(0,0,0,.05)}.pete-reader-scroll{--reader-fs:1.08rem}.pete-reader-body{font-size:var(--reader-fs);line-height:1.75}.pete-reader-body p{margin-bottom:1.05em}.pete-reader-body p:last-child{margin-bottom:0}.pete-reader-scroll.is-size-s{--reader-fs:0.98rem}.pete-reader-scroll.is-size-m{--reader-fs:1.08rem}.pete-reader-scroll.is-size-l{--reader-fs:1.22rem}.pete-reader-scroll.is-size-xl{--reader-fs:1.4rem}.pete-reader-scroll.is-serif .pete-reader-body{font-family:Georgia,Iowan Old Style,Times New Roman,serif}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow:0 0 0 .35rem color-mix(in srgb,var(--accent) 32%,transparent)}.pete-reader-note{margin-top:1.25rem;padding-top:1rem;border-top:1px solid rgba(0,0,0,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.12)}.pete-reader-note a{color:var(--accent);font-weight:700}.pete-reader-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.55)}.pete-disc[data-chip="5"]{--chip:#5aa9e6}.pete-disc[data-chip="25"]{--chip:#4caf7d}.pete-disc[data-chip="100"]{--chip:#2b2118}.pete-disc[data-chip="500"]{--chip:#b079d6}.pete-chip{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.pete-stack{position:absolute;inset:0;pointer-events:none}.pete-stack .pete-disc{position:absolute;left:50%;top:50%;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-fly-layer{position:fixed;inset:0;z-index:60;pointer-events:none;overflow:hidden}.pete-fly{position:absolute;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;will-change:transform}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.36rem*var(--stack, 3))}.pete-rack span[data-chip="5"]{--chip:#5aa9e6}.pete-rack span[data-chip="25"]{--chip:#4caf7d}.pete-rack span[data-chip="100"]{--chip:#2b2118}.pete-rack span[data-chip="500"]{--chip:#b079d6}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(0,0,0,.3);stroke-width:3.5}.pete-gallows-body>*{stroke:#fff;stroke-width:5;opacity:0;filter:drop-shadow(0 2px 0 rgba(0,0,0,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.97)}@keyframes pete-shake{10%,90%{transform:translateX(-1.5px)}20%,80%{transform:translateX(3px)}30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.5rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.4rem;font-weight:700;color:#fff;background:rgba(0,0,0,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.45)}.pete-meter-value{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--accent)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.6rem;background:color-mix(in srgb,var(--ink) 6%,transparent);border:2px solid color-mix(in srgb,var(--ink) 10%,transparent);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-weight:700;color:var(--ink);transition:transform .08s ease,background .15s ease,opacity .15s ease}.pete-key:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 12%,transparent)}.pete-key:active:not(:disabled){transform:translateY(1px) scale(.96)}.pete-key:disabled{cursor:default}.pete-key[data-state=hit]{background:#4caf7d;border-color:#3d9367;color:#fff;opacity:1}.pete-key[data-state=miss]{background:color-mix(in srgb,var(--ink) 4%,transparent);color:color-mix(in srgb,var(--ink) 30%,transparent);text-decoration:line-through}.pete-key:disabled[data-state=""]{opacity:.4}.pete-tier{background:color-mix(in srgb,var(--ink) 4%,transparent);border-color:color-mix(in srgb,var(--ink) 10%,transparent)}.pete-tier:hover{background:color-mix(in srgb,var(--ink) 8%,transparent)}.pete-tier[data-on="1"]{background:color-mix(in srgb,var(--accent) 18%,transparent);border-color:var(--accent)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.pete-solitaire{--card-w:clamp(2.5rem,10.6vw,5.2rem);--card-h:calc(var(--card-w)*1.4);--fan-up:calc(var(--card-h)*0.29);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);cursor:pointer}.pete-stock:hover{filter:brightness(1.08)}.pete-stock:active{transform:translateY(1px) scale(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.pete-waste{position:relative;display:flex;height:var(--card-h);min-width:var(--card-w)}.pete-waste .pete-card+.pete-card{margin-left:calc(var(--card-w)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.5rem;align-items:start;min-height:calc(var(--card-h)*2.6)}.pete-col{display:flex;flex-direction:column;align-items:center;min-height:var(--card-h)}.pete-col .pete-card{position:relative}.pete-col .pete-card[data-face=down]+.pete-card{margin-top:calc(var(--fan-down) - var(--card-h))}.pete-col .pete-card[data-face=up]+.pete-card{margin-top:calc(var(--fan-up) - var(--card-h))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.pete-rail{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem 1.5rem}@media (min-width:1024px){.pete-rail{flex-direction:column;justify-content:flex-start;width:9.5rem;gap:1.5rem}}.pete-rack[data-at=rail]{position:static;justify-content:center}.pete-uno[data-c=red]{--play:#d64545}.pete-uno[data-c=blue]{--play:#3d7fd6}.pete-uno[data-c=yellow]{--play:#e0b02c}.pete-uno[data-c=green]{--play:#46a86b}.pete-uno:before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(80% 55% at 50% 45%,var(--play,transparent),transparent 70%);opacity:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;overflow:hidden;background:var(--uno,#2b2118);color:#fff}.pete-uno-card[data-c=red]{--uno:#d64545}.pete-uno-card[data-c=blue]{--uno:#3d7fd6}.pete-uno-card[data-c=yellow]{--uno:#e0b02c}.pete-uno-card[data-c=green]{--uno:#46a86b}.pete-uno-card[data-c=wild]{--uno:#2b2118}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.pete-uno-oval{position:relative;z-index:1;display:grid;place-items:center;height:78%;width:62%;border-radius:50%;transform:rotate(-24deg);background:#fff;color:var(--uno,#2b2118);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.45rem;font-weight:700;line-height:1;text-shadow:0 1px 0 rgba(0,0,0,.1)}.pete-uno-card[data-c=wild] .pete-uno-oval{color:#2b2118;font-size:1.15rem}.pete-uno-corner{position:absolute;z-index:1;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.5rem)}.pete-uno-hand .pete-uno-card[data-on="1"]:hover{transform:translateY(-1.1rem) scale(1.03)}.pete-uno-hand .pete-uno-card[data-on="0"]{filter:brightness(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.12);transform:translateY(-2px)}.pete-uno-seat[data-uno="1"] .pete-uno-count{color:#ffd76e}.pete-uno-fan{display:flex;--uno-h:3.2rem;--uno-w:2.15rem}.pete-uno-fan .pete-uno-card{margin-left:-1.35rem;transform:rotate(calc((var(--i, 0) - (var(--n, 1) - 1)/2)*4deg));transform-origin:bottom center;box-shadow:0 2px 0 rgba(0,0,0,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{transform:none}30%{transform:translateY(-4px) rotate(-3deg)}65%{transform:translateY(2px) rotate(2deg)}}.pete-uno-discard{display:grid;place-items:center;height:6.4rem;width:4.3rem;border-radius:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(0,0,0,.3)}.pete-uno-colour[data-c=red]{background:#d64545}.pete-uno-colour[data-c=blue]{background:#3d7fd6}.pete-uno-colour[data-c=yellow]{background:#e0b02c;color:#2b2118}.pete-uno-colour[data-c=green]{background:#46a86b}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .9s ease forwards}.pete-uno-badge[data-tone=bad]{background:#cc3d4a;color:#fff}.pete-uno-badge[data-tone=uno]{background:#e0b02c}@keyframes pete-uno-badge{0%{transform:translate(-50%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.75rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1rem;font-weight:700;color:#fff;background:var(--sw,#666);box-shadow:0 3px 0 rgba(0,0,0,.3);transition:transform .12s ease,filter .12s ease}.pete-uno-swatch:hover{transform:translateY(-2px);filter:brightness(1.08)}.pete-uno-swatch:active{transform:translateY(1px)}.pete-uno-swatch[data-c=red]{--sw:#d64545}.pete-uno-swatch[data-c=blue]{--sw:#3d7fd6}.pete-uno-swatch[data-c=yellow]{--sw:#e0b02c;color:#2b2118}.pete-uno-swatch[data-c=green]{--sw:#46a86b}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-discard,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-discard{height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.bg-theme-music{background-color:#b079d6}.bg-theme-anime{background-color:#ec5e8a}.bg-theme-foss{background-color:#d97706}.bg-theme-kids{background-color:#14b8a6}.bg-theme-finance{background-color:#10b981}.bg-theme-lego{background-color:#d01012}.bg-theme-adventure{background-color:#6d4bd8}.text-theme-gaming{color:#2d8a5a}.text-theme-tech{color:#2f7fb8}.text-theme-politics{color:#b8523a}.text-theme-eu{color:#039}.text-theme-music{color:#8a4fb8}.text-theme-anime{color:#c33a6a}.text-theme-foss{color:#b8530a}.text-theme-kids{color:#0f766e}.text-theme-finance{color:#059669}.text-theme-lego{color:#b00d0e}.text-theme-adventure{color:#5836b8}.decoration-theme-gaming{text-decoration-color:#4caf7d}.decoration-theme-tech{text-decoration-color:#5aa9e6}.decoration-theme-politics{text-decoration-color:#e07a5f}.decoration-theme-eu{text-decoration-color:#039}.decoration-theme-music{text-decoration-color:#b079d6}.decoration-theme-anime{text-decoration-color:#ec5e8a}.decoration-theme-foss{text-decoration-color:#d97706}.decoration-theme-kids{text-decoration-color:#14b8a6}.decoration-theme-finance{text-decoration-color:#10b981}.decoration-theme-lego{text-decoration-color:#d01012}.decoration-theme-adventure{text-decoration-color:#6d4bd8}.border-theme-gaming{border-color:#4caf7d}.border-theme-tech{border-color:#5aa9e6}.border-theme-politics{border-color:#e07a5f}.border-theme-eu{border-color:#039}.border-theme-music{border-color:#b079d6}.border-theme-anime{border-color:#ec5e8a}.border-theme-foss{border-color:#d97706}.border-theme-kids{border-color:#14b8a6}.border-theme-finance{border-color:#10b981}.border-theme-lego{border-color:#d01012}.border-theme-adventure{border-color:#6d4bd8}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.paywall-stamp{position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;z-index:5;background:rgba(255,250,240,.18)}.paywall-stamp:before{content:"PAYWALLED";font-family:var(--font-display,ui-sans-serif),system-ui,sans-serif;font-weight:900;font-size:clamp(1rem,3.2vw,1.75rem);letter-spacing:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}html[data-phase=dawn]{--bg:#ffe7d6;--bg-grad:#ffc9c9;--card:#fff4ea;--ink:#4a2e2a;--accent:#ff8a65}html[data-phase=dusk]{--bg:#ffd6a8;--bg-grad:#f7a07e;--card:#fff1de;--ink:#3d2417;--accent:#e6553a}html[data-phase=night]{--bg:#1a1f3a;--bg-grad:#2a3358;--card:#2d365a;--ink:#f1ecd8;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,var(--felt-a) 0,var(--felt-b) 55%,var(--felt-c) 100%)}@media (prefers-reduced-motion:reduce){html[data-room=casino-night] .cs-room:before{animation:none}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.14)}.cs-stack[data-chip="5"]{--chip:#5aa9e6}.cs-stack[data-chip="25"]{--chip:#4caf7d}.cs-stack[data-chip="100"]{--chip:#2b2118}.cs-stack[data-chip="500"]{--chip:#b079d6}.pete-poker{--seat-w:7.5rem}.pete-poker-seats{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:.75rem}.pete-seat{position:relative;display:flex;flex-direction:column;align-items:center;gap:.4rem;width:var(--seat-w);transition:opacity .3s ease,filter .3s ease}.pete-seat[data-state=folded]{opacity:.38;filter:grayscale(.7)}.pete-seat[data-state=out]{opacity:.2}.pete-seat[data-turn="1"] .pete-seat-plate{box-shadow:0 0 0 2px var(--accent),0 0 1.6rem -.2rem var(--accent);animation:pete-seat-wait 1.6s ease-in-out infinite}@keyframes pete-seat-wait{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}.pete-seat-cards{display:flex;justify-content:center;gap:.15rem;min-height:4.4rem;--card-h:4.4rem;--card-w:3.15rem}.pete-seat-cards[data-mucked="1"]{opacity:0;transform:translateY(-.6rem) scale(.9);transition:all .35s ease}.pete-seat-plate{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;padding:.3rem .4rem;border-radius:.85rem;background:rgba(0,0,0,.32);border:1px solid hsla(0,0%,100%,.12);transition:box-shadow .25s ease}.pete-seat-name{font-family:var(--font-display,inherit);font-weight:700;font-size:.8rem;line-height:1.1;color:hsla(0,0%,100%,.92);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pete-seat-stack{font-weight:700;font-size:.78rem;font-variant-numeric:tabular-nums;color:hsla(0,0%,100%,.58)}.pete-seat[data-state=allin] .pete-seat-stack:after{content:" all in";color:var(--accent)}.pete-seat-pos{position:absolute;top:-.5rem;right:-.5rem;display:grid;place-items:center;min-width:1.45rem;height:1.45rem;padding:0 .3rem;border-radius:999px;font-size:.58rem;font-weight:800;letter-spacing:.02em;background:hsla(0,0%,100%,.9);color:#2b2118;box-shadow:0 2px 0 rgba(0,0,0,.3)}.pete-seat-pos[data-pos=BTN]{background:#f5d76e}.pete-seat-spot{height:3.6rem;width:3.6rem;margin-bottom:.5rem}.pete-seat-spot .pete-disc{height:1.6rem;width:1.6rem;margin:-.8rem 0 0 -.8rem}.pete-seat-spot .pete-spot-total{font-size:.66rem;padding:.05rem .45rem}.pete-poker-middle{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:.5rem 0}.pete-poker-pot{margin-top:.9rem}.pete-poker-board{display:flex;gap:.35rem;min-height:6rem;--card-h:6rem;--card-w:4.3rem}.pete-poker-pot{display:flex;flex-direction:column;align-items:center;gap:.1rem}.pete-poker-pot-pile{position:relative;width:7rem;height:3rem}.pete-poker-pot-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:hsla(0,0%,100%,.42)}.pete-poker-pot-total{font-family:var(--font-display,inherit);font-size:1.5rem;font-weight:800;font-variant-numeric:tabular-nums;color:#fff;text-shadow:0 2px 0 rgba(0,0,0,.28)}.pete-poker-side{font-size:.68rem;font-weight:700;color:hsla(0,0%,100%,.5)}.pete-poker-you .pete-seat{width:auto}.pete-poker-you .pete-seat-cards{--card-h:6.8rem;--card-w:4.85rem;gap:.3rem;min-height:6.8rem}.pete-poker-you .pete-seat-plate{padding:.4rem 1.1rem}.pete-poker-you .pete-seat-name,.pete-poker-you .pete-seat-stack{font-size:.95rem}.pete-poker-verdict{border-radius:999px;background:hsla(0,0%,100%,.95);padding:.5rem 1.25rem;font-family:var(--font-display,inherit);font-size:1.05rem;font-weight:700;color:#2b2118;box-shadow:0 4px 0 rgba(0,0,0,.18)}.pete-poker-verdict[data-tone=win]{background:#4caf7d;color:#fff}.pete-poker-verdict[data-tone=lose]{background:hsla(0,0%,100%,.9);color:#7a5c50}.pete-raise{display:flex;align-items:center;gap:.6rem;flex:1 1 14rem;min-width:0}.pete-raise input[type=range]{flex:1;min-width:0;accent-color:var(--accent)}.pete-raise-to{font-family:var(--font-display,inherit);font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums;min-width:3.5rem;text-align:right}.pete-poker-log{max-height:7rem;overflow-y:auto;font-size:.75rem;line-height:1.5;color:hsla(0,0%,100%,.55)}.pete-poker-log b{color:hsla(0,0%,100%,.85);font-weight:700}@media (max-width:640px){.pete-poker{--seat-w:5.6rem}.pete-seat-cards{--card-h:3.5rem;--card-w:2.5rem;min-height:3.5rem}.pete-poker-board{--card-h:5rem;--card-w:3.55rem;min-height:5rem;gap:.25rem}.pete-poker-you .pete-seat-cards{--card-h:6rem;--card-w:4.3rem;min-height:6rem}.pete-poker-pot-total{font-size:1.25rem}}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}} \ No newline at end of file diff --git a/internal/web/static/js/holdem.js b/internal/web/static/js/holdem.js new file mode 100644 index 0000000..6e15c6d --- /dev/null +++ b/internal/web/static/js/holdem.js @@ -0,0 +1,648 @@ +// The hold'em table. +// +// Same bargain as every other table in the room: the browser holds no game. It +// sends one move, and what comes back is that move *and every bot action behind +// it* — plus whatever streets that finished and whatever the pot did — as a +// script of events. So a round trip here can be a whole hand: shove all-in and +// the flop, turn, river, showdown and payout all arrive in one response, and this +// file's job is to play them back slowly enough that you can watch it happen to +// you. +// +// The one thing here that no other table has is a *second seat with money on it*. +// Everywhere else the spot is a singleton, because there was only ever you and +// the house. Here every seat has its own, chips move from a seat to its spot and +// from every spot into the pot, and out of the pot to whoever won it. PeteFX.spot +// still owns the rule that the number under a pile is a readout of that pile, so +// none of that arithmetic lives in here. +// +// And the browser never learns a bot's hand. Their cards are backs until a +// showdown turns them over, because a showdown is the only time a player at a +// real table would be looking at them. +(function () { + "use strict"; + + var root = document.querySelector("[data-holdem]"); + if (!root) return; + + var FX = window.PeteFX; + var Cards = window.PeteCards; + + var seatsEl = root.querySelector("[data-seats]"); + var youEl = root.querySelector("[data-you]"); + var boardEl = root.querySelector("[data-board]"); + var potStack = root.querySelector("[data-pot-stack]"); + var potTotal = root.querySelector("[data-pot-total]"); + var sideEl = root.querySelector("[data-side]"); + var blindsEl = root.querySelector("[data-blinds]"); + var tableName = root.querySelector("[data-table-name]"); + var verdictEl = root.querySelector("[data-verdict]"); + var houseEl = root.querySelector("[data-house]"); + + var acting = root.querySelector("[data-acting]"); + var between = root.querySelector("[data-between]"); + var sitting = root.querySelector("[data-sitting]"); + + var foldBtn = root.querySelector('[data-move="fold"]'); + var checkBtn = root.querySelector('[data-move="check"]'); + var callBtn = root.querySelector('[data-move="call"]'); + var raiseBtn = root.querySelector('[data-move="raise"]'); + var callAmt = root.querySelector("[data-call-amount]"); + var raiseRow = root.querySelector("[data-raise-row]"); + var slider = root.querySelector("[data-raise-slider]"); + var raiseTo = root.querySelector("[data-raise-to]"); + var raiseLbl = root.querySelector("[data-raise-label]"); + var raiseVerb = root.querySelector("[data-raise-verb]"); + + var dealBtn = root.querySelector("[data-deal]"); + var topupBtn = root.querySelector("[data-topup]"); + var leaveBtn = root.querySelector("[data-leave]"); + var stackEl = root.querySelector("[data-table-stack]"); + var boughtEl = root.querySelector("[data-bought-in]"); + var rakeEl = root.querySelector("[data-session-rake]"); + + var sitBtn = root.querySelector("[data-sit]"); + var buySlider = root.querySelector("[data-buyin-slider]"); + var buyLabel = root.querySelector("[data-buyin]"); + var buyNote = root.querySelector("[data-buyin-note]"); + var botsNote = root.querySelector("[data-bots-note]"); + var gameMsg = root.querySelector("[data-game-msg]"); + var betweenMsg = root.querySelector("[data-between-msg]"); + var tableMsg = root.querySelector("[data-table-msg]"); + + var view = null; // the table, as the server last described it + var busy = false; + var seatEls = []; // one per seat: { root, cards, plate, stack, spot } + var shown = []; // what each seat's stack label currently reads + var pot = null; // the middle pile, a PeteFX.spot + + var tierBtns = Array.prototype.slice.call(root.querySelectorAll("[data-tier]")); + var botBtns = Array.prototype.slice.call(root.querySelectorAll("[data-bot-count]")); + var tier = null; + var bots = 2; + var buyIn = 0; + + function money(n) { return (n || 0).toLocaleString(); } + + function say(el, text, tone) { + if (!el) return; + if (!text) { el.classList.add("hidden"); return; } + el.textContent = text; + el.classList.remove("hidden"); + el.style.color = tone === "bad" ? "#cc3d4a" : ""; + } + + // ---- building the felt ---------------------------------------------------- + + // seat builds one player: their cards, their name and stack, and the spot their + // bet sits on. Bots go in the row along the top; you get your own, bigger. + function seat(s, i, mine) { + var wrap = document.createElement("div"); + wrap.className = "pete-seat"; + wrap.dataset.seat = i; + wrap.dataset.state = s.state; + + var cards = document.createElement("div"); + cards.className = "pete-seat-cards"; + + var plate = document.createElement("div"); + plate.className = "pete-seat-plate"; + var name = document.createElement("span"); + name.className = "pete-seat-name"; + name.textContent = s.name; + var stack = document.createElement("span"); + stack.className = "pete-seat-stack"; + stack.textContent = money(s.stack); + plate.appendChild(name); + plate.appendChild(stack); + + // The button, the blinds. It hangs off the name plate rather than the seat, + // because the seat's corner is a different place for you than for a bot — your + // bet spot is above your cards and theirs is below — and a badge that floats + // over an empty betting circle reads as a bug. + if (s.pos) { + var pos = document.createElement("span"); + pos.className = "pete-seat-pos"; + pos.dataset.pos = s.pos; + pos.textContent = s.pos; + plate.appendChild(pos); + } + + var spotEl = document.createElement("div"); + spotEl.className = "pete-spot pete-seat-spot"; + var pile = document.createElement("div"); + pile.className = "pete-stack"; + var total = document.createElement("span"); + // The shared class, not one of our own: it hangs the number *below* the ring, + // which is what keeps the chips from landing on top of it. + total.className = "pete-spot-total"; + spotEl.appendChild(pile); + spotEl.appendChild(total); + + // Your bet sits between you and the board, so it goes above your cards; a + // bot's sits between them and the board, so it goes below theirs. + if (mine) { + wrap.appendChild(spotEl); + wrap.appendChild(cards); + wrap.appendChild(plate); + } else { + wrap.appendChild(cards); + wrap.appendChild(plate); + wrap.appendChild(spotEl); + } + + var api = FX.spot({ spot: spotEl, stack: pile, total: total }); + api.render(s.bet); + + paintCards(cards, s, mine); + return { root: wrap, cards: cards, plate: plate, stackEl: stack, spot: api }; + } + + // paintCards puts the two cards in front of a seat. A bot's are backs, unless + // the server has actually sent us faces — which it only ever does at a showdown. + function paintCards(el, s, mine) { + el.innerHTML = ""; + if (s.state === "out") return; + var faces = s.cards || []; + var n = faces.length ? faces.length : (s.state === "folded" ? 0 : 2); + for (var i = 0; i < n; i++) { + el.appendChild(Cards.el(faces[i] || null, { deal: false, tilt: !mine })); + } + } + + function render(v) { + view = v; + + // The seats along the top, and you underneath. + seatsEl.innerHTML = ""; + youEl.innerHTML = ""; + seatEls = []; + shown = []; + v.seats.forEach(function (s, i) { + var mine = i === 0; + var built = seat(s, i, mine); + seatEls[i] = built; + shown[i] = s.stack; + (mine ? youEl : seatsEl).appendChild(built.root); + built.root.dataset.turn = (v.phase === "betting" && v.to_act === i) ? "1" : "0"; + }); + + // The board. + boardEl.innerHTML = ""; + (v.board || []).forEach(function (c) { + boardEl.appendChild(Cards.el(c, { deal: false, tilt: false })); + }); + + // The pot. Its chips live in a box of their own — see .pete-poker-pot-pile — + // so the number underneath stays readable. + pot = FX.spot({ spot: potStack.parentNode, stack: potStack, total: null }); + pot.render(v.pot); + potTotal.textContent = money(v.pot); + blindsEl.textContent = v.tier.sb + "/" + v.tier.bb; + tableName.textContent = v.tier.name; + if (v.side && v.side.length > 1) { + sideEl.textContent = v.side.map(function (n, i) { + return (i === 0 ? "main " : "side ") + money(n); + }).join(" · "); + sideEl.classList.remove("hidden"); + } else { + sideEl.classList.add("hidden"); + } + + stackEl.textContent = money(v.stack); + boughtEl.textContent = money(v.bought_in); + rakeEl.textContent = money(v.rake); + + panels(); + } + + // panels decides which of the three bars is showing: the one that acts, the one + // between hands, or the one you sit down from. + function panels() { + // A session you have got up from is not a live one: the felt still shows the + // last hand, but the table you sit down at is the one that's open to you. + var live = !!view && view.phase !== "done"; + sitting.classList.toggle("hidden", live); + acting.classList.toggle("hidden", !live || view.phase !== "betting" || view.to_act !== 0); + between.classList.toggle("hidden", !live || view.phase !== "handover"); + if (!live) return; + + if (view.phase === "betting" && view.to_act === 0) { + checkBtn.classList.toggle("hidden", !view.can_check); + callBtn.classList.toggle("hidden", view.can_check); + callAmt.textContent = money(view.owed); + + raiseRow.classList.toggle("hidden", !view.can_raise); + if (view.can_raise) { + slider.min = view.min_raise_to; + slider.max = view.max_raise_to; + slider.step = Math.max(1, view.tier.bb / 2); + slider.value = Math.min(view.max_raise_to, view.min_raise_to); + // "Bet" when nobody has, "Raise to" when somebody has. It is the same + // move and the same button, but calling a bet a raise is how you tell a + // player who has never played that this table is confused. + raiseVerb.textContent = view.owed > 0 ? "Raise to" : "Bet"; + showRaise(); + } + } + if (view.phase === "handover") { + topupBtn.disabled = !view.max_topup; + topupBtn.textContent = view.max_topup ? "Top up " + money(view.max_topup) : "Top up"; + } + } + + function showRaise() { + var to = Number(slider.value); + raiseTo.textContent = money(to); + raiseLbl.textContent = money(to); + } + + // ---- playing the script --------------------------------------------------- + + var STREETS = { flop: 1, turn: 1, river: 1 }; + + function wait(ms) { + return new Promise(function (r) { setTimeout(r, FX.reduced ? 0 : ms); }); + } + + // play walks the events the server sent, one beat at a time, and only then + // re-renders the table it ended on. Everything the player is meant to see + // happening happens here; render() is the state it settles into. + function play(events, final) { + var chain = Promise.resolve(); + if (!events || !events.length) { render(final); return chain; } + + events.forEach(function (e) { + chain = chain.then(function () { return beat(e, final); }); + }); + return chain.then(function () { + render(final); + verdict(events, final); + }); + } + + function beat(e, final) { + var s = seatEls[e.seat]; + + switch (e.kind) { + case "hand": + // A new deal: clear the felt before anything lands on it. + boardEl.innerHTML = ""; + verdictEl.classList.add("hidden"); + seatEls.forEach(function (x) { x.spot.render(0); x.cards.innerHTML = ""; }); + pot.render(0); + potTotal.textContent = "0"; + sideEl.classList.add("hidden"); + return wait(140); + + case "rebuy": + if (s) { shown[e.seat] = e.total; FX.count(s.stackEl, e.total); } + return wait(220); + + case "blind": + if (!s) return; + moveStack(e.seat, -e.amount); + return s.spot.pour(s.plate, e.amount); + + case "hole": + // Two cards to everybody, round the table, as they are actually dealt. + return dealHoles(final); + + case "action": + return action(e, final); + + case "flop": + case "turn": + case "river": + return street(e); + + case "pot": + // The bets in front of the seats have been swept in. Nothing else in the + // script says so, and the pot is about to be paid out of. + return collect(e.amount); + + case "uncalled": + if (!s) return; + return s.spot.sweep(s.plate).then(function () { moveStack(e.seat, e.amount); }); + + case "show": + if (!s) return; + paintCards(s.cards, { state: "active", cards: e.cards }, e.seat === 0); + flash(s.root); + return wait(420); + + case "rake": + // The house takes its cut out of the pot, in front of you, so it is a + // thing that visibly happens rather than a number that quietly differs. + return pot.sweep(houseEl, e.amount).then(function () { + potTotal.textContent = money(pot.amount); + return wait(160); + }); + + case "win": + if (!s) return; + return pot.sweep(s.plate, e.amount, { gap: 55 }).then(function () { + potTotal.textContent = money(pot.amount); + moveStack(e.seat, e.amount); + if (e.seat === 0 && e.amount > 0) FX.burst(s.plate, { count: 18 }); + return wait(260); + }); + + case "end": + return wait(280); + } + return Promise.resolve(); + } + + // dealHoles puts two cards in front of everyone still in the hand. Yours land + // face up; theirs land as backs, because that is all that came over the wire. + function dealHoles(final) { + var chain = Promise.resolve(); + for (var round = 0; round < 2; round++) { + (function (round) { + chain = chain.then(function () { + var beats = []; + final.seats.forEach(function (s, i) { + if (s.state === "out") return; + var built = seatEls[i]; + if (!built) return; + var face = (i === 0 && s.cards) ? s.cards[round] : null; + var card = Cards.el(face, { deal: true, tilt: i !== 0 }); + built.cards.appendChild(card); + beats.push(wait(70 * i)); + }); + return Promise.all(beats).then(function () { return wait(180); }); + }); + })(round); + } + return chain; + } + + // action animates one seat doing one thing. + function action(e, final) { + var s = seatEls[e.seat]; + if (!s) return Promise.resolve(); + + if (e.text === "fold") { + s.root.dataset.state = "folded"; + s.cards.dataset.mucked = "1"; + return wait(320); + } + if (e.text === "check") { + flash(s.root); + return wait(320); + } + // call, raise, allin: chips leave their stack for their spot. + if (!e.amount) return wait(200); + moveStack(e.seat, -e.amount); + return s.spot.pour(s.plate, e.amount).then(function () { + if (e.text === "allin") flash(s.root); + return wait(180); + }); + } + + // collect sweeps every seat's bet into the middle. The total is worked out up + // front rather than accumulated as the chips land, because the sweeps run at the + // same time and would otherwise race each other into the pot's counter. + function collect(total) { + var moved = 0; + var sweeps = seatEls.map(function (s) { + if (!s || s.spot.amount <= 0) return Promise.resolve(); + moved += s.spot.amount; + return s.spot.sweep(potStack.parentNode, s.spot.amount, { gap: 30 }); + }); + if (!moved) { + if (total != null) { pot.render(total); potTotal.textContent = money(total); } + return Promise.resolve(); + } + var to = total != null ? total : pot.amount + moved; + return Promise.all(sweeps).then(function () { + pot.render(to); + potTotal.textContent = money(to); + return wait(200); + }); + } + + // street sweeps the bets in, then turns the cards. + function street(e) { + return collect(e.amount).then(function () { + // The board turns one card at a time, even the flop. Three cards appearing + // at once is a screenshot; three cards appearing in a row is a flop. + var chain = Promise.resolve(); + (e.cards || []).forEach(function (c) { + chain = chain.then(function () { + boardEl.appendChild(Cards.el(c, { deal: true, tilt: false })); + return wait(240); + }); + }); + return chain; + }).then(function () { + return wait(200); + }); + } + + // moveStack keeps a seat's stack label honest *while the chips are moving*. The + // authoritative number is always the server's, and render() puts it back at the + // end of the script — but a stack that only updates then would sit unchanged + // through the whole hand and then jump, which reads as the table correcting + // itself rather than as chips being paid. + function moveStack(i, delta) { + var s = seatEls[i]; + if (!s) return; + shown[i] = Math.max(0, (shown[i] || 0) + delta); + FX.count(s.stackEl, shown[i]); + } + + function flash(el) { + el.animate( + [{ transform: "scale(1)" }, { transform: "scale(1.06)" }, { transform: "scale(1)" }], + { duration: 320, easing: "ease-out" } + ); + } + + // verdict says what the hand did to you, once, in words. + function verdict(events, final) { + var won = 0, showed = false, busted = false; + events.forEach(function (e) { + if (e.kind === "win" && e.seat === 0) won += e.amount; + if (e.kind === "show") showed = true; + if (e.kind === "bust") busted = true; + }); + if (busted) { + show("You're out of chips. Sit down again when you're ready.", "lose"); + return; + } + if (!events.some(function (e) { return e.kind === "end"; })) return; + + var me = final.seats[0]; + if (won > 0) { + show(showed + ? "You win " + money(won) + " with " + article(handName(events)) + "." + : "They folded. You take " + money(won) + ".", "win"); + } else if (me.state === "folded") { + show("Folded.", "lose"); + } else { + show("No good this time.", "lose"); + } + + function show(text, tone) { + verdictEl.textContent = text; + verdictEl.dataset.tone = tone; + verdictEl.classList.remove("hidden"); + } + } + + function handName(events) { + var mine = events.filter(function (e) { return e.kind === "show" && e.seat === 0; })[0]; + return mine && mine.text ? mine.text : "the best hand"; + } + + // "You win 975 with straight" is not a sentence. Most hands take an article and + // the counted ones don't. + function article(desc) { + if (/^(two pair|three of a kind|four of a kind|high card|the best hand)$/.test(desc)) return desc; + return "a " + desc; + } + + // ---- talking to the table -------------------------------------------------- + + function send(body, msgEl) { + if (busy) return Promise.resolve(); + busy = true; + say(msgEl, ""); + // Whatever the last hand said about itself stops being true the moment you do + // something. Only the "hand" beat used to clear this, so a verdict could linger + // over a hand it had nothing to do with. + verdictEl.classList.add("hidden"); + return window.PeteGames + .post("/api/games/holdem/move", body) + .then(function (v) { + window.PeteGames.apply(v); + return play(v.holdem_events, v.holdem || null).then(function () { + if (!v.holdem) { render0(); return; } + if (v.holdem.phase === "done") { + var got = v.holdem.payout, put = v.holdem.bought_in; + say(tableMsg, got > put + ? "You got up " + money(got - put) + " ahead. " + money(got) + " back on your stack." + : got === 0 + ? "Cleaned out. Better luck at the next table." + : "You got up " + money(put - got) + " down. " + money(got) + " back on your stack."); + setTimeout(render0, 2600); + } + }); + }) + .catch(function (err) { say(msgEl, err.message, "bad"); }) + .then(function () { busy = false; }); + } + + // render0 is the table with nobody at it. + function render0() { + view = null; + seatsEl.innerHTML = ""; + youEl.innerHTML = ""; + boardEl.innerHTML = ""; + potStack.innerHTML = ""; + potTotal.textContent = "0"; + sideEl.classList.add("hidden"); + panels(); + syncSit(); + } + + if (foldBtn) foldBtn.addEventListener("click", function () { send({ move: "fold" }, gameMsg); }); + if (checkBtn) checkBtn.addEventListener("click", function () { send({ move: "check" }, gameMsg); }); + if (callBtn) callBtn.addEventListener("click", function () { send({ move: "call" }, gameMsg); }); + if (raiseBtn) raiseBtn.addEventListener("click", function () { + var to = Number(slider.value); + // Sliding all the way to the top is shoving, and the table would rather be + // told that than be told to raise to exactly everything you have. + send(to >= view.max_raise_to ? { move: "allin" } : { move: "raise", to: to }, gameMsg); + }); + + if (slider) slider.addEventListener("input", showRaise); + root.querySelectorAll("[data-raise-preset]").forEach(function (b) { + b.addEventListener("click", function () { + var which = b.dataset.raisePreset; + var to; + if (which === "max") to = view.max_raise_to; + else to = view.owed + view.pot * Number(which) + (view.pot > 0 ? view.owed : 0); + to = Math.max(view.min_raise_to, Math.min(view.max_raise_to, Math.round(to))); + slider.value = to; + showRaise(); + }); + }); + + if (dealBtn) dealBtn.addEventListener("click", function () { send({ move: "deal" }, betweenMsg); }); + if (leaveBtn) leaveBtn.addEventListener("click", function () { send({ move: "leave" }, betweenMsg); }); + if (topupBtn) topupBtn.addEventListener("click", function () { + send({ move: "topup", amount: view.max_topup }, betweenMsg); + }); + + // ---- sitting down ---------------------------------------------------------- + + function pickTier(btn) { + tier = btn; + tierBtns.forEach(function (b) { b.dataset.on = b === btn ? "1" : "0"; }); + var min = Number(btn.dataset.min), max = Number(btn.dataset.max), bb = Number(btn.dataset.bb); + buySlider.min = min; + buySlider.max = max; + buySlider.step = bb; + buySlider.value = Math.min(max, Math.max(min, 50 * bb)); // fifty big blinds, the default anybody sensible picks + syncSit(); + } + + function pickBots(btn) { + bots = Number(btn.dataset.botCount); + botBtns.forEach(function (b) { b.dataset.on = b === btn ? "1" : "0"; }); + syncSit(); + } + + function syncSit() { + if (!tier) return; + buyIn = Number(buySlider.value); + var bb = Number(tier.dataset.bb); + buyLabel.textContent = money(buyIn); + buyNote.textContent = Math.round(buyIn / bb) + " big blinds. Short is fewer decisions; deep is more of them."; + botsNote.textContent = bots === 1 + ? "Heads up. The bots know this game best when there's only one of them." + : bots + " bots. More opponents, and a hand has to be better to be worth playing."; + + var chips = window.PeteGames.view(); + sitBtn.disabled = !chips || chips.chips < buyIn; + say(tableMsg, sitBtn.disabled ? "You need " + money(buyIn) + " chips to sit at this table." : ""); + } + + tierBtns.forEach(function (b) { b.addEventListener("click", function () { pickTier(b); }); }); + botBtns.forEach(function (b) { b.addEventListener("click", function () { pickBots(b); }); }); + if (buySlider) buySlider.addEventListener("input", syncSit); + + if (sitBtn) sitBtn.addEventListener("click", function () { + if (busy || !tier) return; + busy = true; + say(tableMsg, ""); + window.PeteGames + .post("/api/games/holdem/sit", { + tier: tier.dataset.tier, + bots: bots, + buyin: Number(buySlider.value), + }) + .then(function (v) { + window.PeteGames.apply(v); + render(v.holdem); + // A table with nobody dealt in yet is a table waiting for you to say go. + say(betweenMsg, "You're in. Deal when you're ready."); + }) + .catch(function (err) { say(tableMsg, err.message, "bad"); }) + .then(function () { busy = false; }); + }); + + // ---- boot ------------------------------------------------------------------ + + window.PeteGames.onUpdate(function () { if (!view) syncSit(); }); + + pickTier(tierBtns[0]); + pickBots(botBtns[1]); + + window.PeteGames.refresh().then(function (v) { + if (v && v.holdem) render(v.holdem); + else render0(); + }); +})(); diff --git a/internal/web/templates/games.html b/internal/web/templates/games.html index 1607fe1..85f1a10 100644 --- a/internal/web/templates/games.html +++ b/internal/web/templates/games.html @@ -106,6 +106,22 @@

+ +
+ ♠️ +
+

Texas Hold'em

+

Buy in. Beat them. Get up.

+
+ Open +
+

+ A real cash game against bots that were trained on it, not scripted. No multiple, no + 3:2 — you leave with whatever is in front of you, less the rake on the pots you win. +

+
+ {{range .Soon}}
diff --git a/internal/web/templates/holdem.html b/internal/web/templates/holdem.html new file mode 100644 index 0000000..abfa84d --- /dev/null +++ b/internal/web/templates/holdem.html @@ -0,0 +1,212 @@ +{{define "title"}}Hold'em · {{.Room.Name}}{{end}} + +{{define "main"}} +
+ +
+
+ + + Back to the casino + +

Texas Hold'em

+
+

Buy in, play as long as you like, leave with what's in front of you

+
+ + {{template "_chipbar" .}} + + +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ Pot + 0 + +
+ +
+ +
+
+ +
+
+ + + +
+
+ + + + + + + + +
+ +
What are you playing for?
+
+ {{range .Stakes}} + + {{end}} +
+ +
+
+
How many of them?
+
+ + + + + +
+

+
+ +
+
Buying in for
+
+ + 0 +
+

+
+
+ + + +

+ 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. +

+ +
+ +
+{{end}} + +{{define "scripts"}} + + + + +{{end}} diff --git a/pete_games_plan.md b/pete_games_plan.md index 84ad661..bea1007 100644 --- a/pete_games_plan.md +++ b/pete_games_plan.md @@ -399,21 +399,106 @@ A multi-session build. This section is the handover; read it before anything els remembering as a rule: **size a card by its vars, never by the box you put it in.** + +- **Hold'em, and it is a cash game.** *(2026-07-14. Built, tested, and driven in a + browser. The bots had to be retrained from scratch — see below, it is the whole + story of this phase.)* + - **You buy in, you play, you leave with what's in front of you.** This is the + only table in the casino that is a *session* rather than a game. Everywhere else + stakes once and pays a multiple; poker isn't that shape. So the live row lives + across hands, and chips cross the border exactly twice: once when you sit down + and once when you get up. In between every pot is inside the engine and storage + sees none of it. Three stakes (1/2, 5/10, 25/50), buy in for 20–100 big blinds, + top up between hands, bust and the session simply ends. + - **The rake is a real cardroom's rake**: five percent of a pot that *sees a flop*, + capped at three big blinds. No flop, no drop — so folding your blind round after + round costs you the blinds and no fee. Still winnings-only in the sense that + matters: you pay it out of a pot you win, never out of a bet you lose. + - **The bots move inside ApplyMove**, as UNO's do, which is what keeps poker off a + socket. One request plays your action, every bot action behind it, and whatever + streets that finishes — so shoving all-in returns the flop, turn, river, showdown + and payout in a single response, as a script the felt plays back. + - **The CFR policy was a lie, twice over, and this is the part worth reading.** + §5 called it "the single highest-value asset in either repository". It was not + being used *at all*, and could not have been: + 1. **The key never matched.** The trainer packed a single "am I last to act" bit + and wrote its keys as `IP`/`OOP`. The runtime looked them up with the labels a + player would recognise — `BTN`, `SB`, `BB`, `UTG`. Not one key ever hit, for + the entire life of the game in gogobee. Nothing looked broken: a policy miss is + not an error, it is a silent fall back to a pot-odds heuristic. The bots played; + they just never once read the five million iterations sitting in policy.gob. + 2. **And it was the wrong game anyway.** `TrainCFR` opens with `stack0, stack1 = 20, + 20` at 1/2 blinds — a **ten big blind** push-fold stack. 82% of its nodes are in + the "stack smaller than the pot" bucket. A 20–100BB cash game lands almost + nowhere near it. Worse, the tree it trained on was not hold'em: a call always + ended the street (so no big-blind option and no check-check), turn order was + history-length parity, and the payoff was ±half the pot regardless of who had + put what in. + - **So the trainer was rewritten to play the real engine.** `internal/games/holdem/ + train.go` + `cmd/holdem-train`. External-sampling MCCFR, every move applied through + `Step` — the same reducer the felt calls — so the blinds, the min-raise, street + completion, side pots and the money are the ones a player actually meets. The + stack depth is drawn fresh every hand across the whole 20–100BB range, because + poker at twenty big blinds and poker at a hundred are different games and a bot + that only knows one folds into the other. + - **The key is built by one function, `State.spot`, called by both the trainer and + the table.** That is the entire fix for bug (1), and it is structural: they cannot + drift apart because there is only one of them. And because a miss is *still* silent, + `Hits`/`Misses` are counted at the point a bot looks itself up, and + `TestTheBotsAreActuallyTrained` fails if the heads-up hit rate drops under 60%. It + is 95%. Multiway degrades on purpose — the policy is heads-up, six-handed reuses it + as a documented approximation, and the rest falls through to pot odds. + - **Three money bugs, and the tests earned their keep.** `TestChipsAreConserved` + plays a hundred sessions of real hands and counts every chip after every move; it + caught an **uncalled bet that minted chips** (the rule skipped folded players when + working out what had been matched, so a river bet folded to came back *whole*, + including the part called on the flop). A Go var-init ordering trap made `deck52` + build from an empty conversion table, so every card was identical, every showdown + tied and **every bot believed it held exactly 50% equity** — package-level vars are + built before `init()` runs. And the browser found the third: **the rake was + silently zero**, because the tiers declared `RakePct: 5` meaning percent while + `New()` overwrites it with blackjack's `0.05` meaning a fraction, and the integer + arithmetic floored 5% of a hundredth of the pot to nothing. Every rake test built + its own `State` by hand and so never saw the number the table runs on. There is now + one that does. + - **Driven in a browser, 2026-07-14, and it holds up.** Sitting down took 500 off a + 5,000 stack and put it on the table; a hand played out; getting up put 1,004 back + (money conserved to the chip). The raise slider, the pot/half-pot/max presets, a + shove that runs the whole board out in one response, and a reload mid-hand that + brings back the hand, the board, the pot, the street and the stack — all clean. + **Side pots and split pots balance**: a three-way all-in paid 758 + 757 + 920 out + of a 2,495 pot with 60 raked, and `paid + rake == pot` on every multi-winner hand + sampled. A bot's cards never cross the wire until a showdown, and a folded seat's + never do at all. Six-handed at 390px: no sideways overflow, nothing colliding. + Console silent. + - Two felt bugs only the browser could show. **`.pete-stack` is `position: absolute; + inset: 0`** — so the pot's chips, sharing a box with the number under them, painted + straight over it: the pot showed a chip and no total. The pile needs a box of its + own. And **a `.pete-spot` is 7rem across** because blackjack has exactly one of + them; six of those is most of a felt, so a seat's bet spot is less than half the + size, with chips scaled to match. The bet total hangs *below* the ring + (`.pete-spot-total`), which is the existing rule for exactly this reason. + ### Next, in order -1. **Phase 4 — hold'em**, as below. It is the last game on the list, and the first - one where a hand has other people in it: the spot is a singleton and the chip - animations are tuned for one seat (see "still open" below). -2. **Deploy.** Hangman, solitaire, trivia and UNO are all played, and all four are - still sitting on main un-deployed — the live casino is blackjack and nothing else. - The server runs `StartTriviaBank`, so trivia's bank fills itself once the binary - is out there, but the first player to try a ladder in the first minute after a - deploy gets the 503. +1. **Deploy.** Hangman, solitaire, trivia, UNO and hold'em are all played and all + five are sitting on main un-deployed — the live casino is blackjack and nothing + else. The server runs `StartTriviaBank`, so trivia's bank fills itself once the + binary is out there, but the first player to try a ladder in the first minute + after a deploy gets the 503. +2. **No Mercy UNO.** The plan's header line has always promised "UNO (normal + + no-mercy)" and only normal was ever built. gogobee has the rules + (`uno_nomercy.go`: a 168-card deck, draw-stacking, elimination at 25 cards, + sudden-death point scoring). It is a *rules dial orthogonal to the table-size + tier*, so the lobby becomes 3 sizes × 2 rule sets — and **the multiples have to be + re-measured**, because the current 2.2×/2.9×/3.6× are priced off a measured + go-out-first rate (43/32/27%) that draw-stacking and mercy elimination change + completely. Shipping No Mercy on the regular tier's prices would misprice it. -Still open on the table itself, none of it blocking: **split** isn't implemented (the -engine has no move for it), the felt is roomy at desktop widths with only one seat on -it, and the chip animations are tuned for one player — a second seat would need the -spot to be per-seat rather than the singleton it is now. +Still open on hold'em, none of it blocking: the policy is **heads-up**, so a +six-handed table is an approximation of it (the hit rate falls from 95% to about 17% +at six seats, and the rest is pot odds) — a multiway policy would want its own +training run with more than two seats in the tree. Blackjack still has no **split**. ### How the browser half fits together @@ -592,20 +677,31 @@ into `pete/internal/games/`, let them drift, no shared module. | Game | Copy | Rewrite | Notes | |---|---|---|---| -| **Hold'em** | ~2,700 LOC | the shell | The crown jewel. Take it all. | +| **Hold'em** | ~1,400 LOC | the shell + **the whole CFR trainer** | See the warning below. | | **UNO** | ~1,400 LOC | the turn engine | Great primitives, unshippable engine. | | **Hangman** | ~250 LOC | loading/persistence | Clean rune-safe state machine. | | **Blackjack** | ~95 LOC | everything else | 95 lines is the entire core. | | **Trivia** | ~80 LOC | everything else | **No question bank exists.** | -### Hold'em — take almost all of it +### Hold'em — take the poker, not the brain + +> **This section was wrong, and it cost most of Phase 4. Read §0's hold'em entry before +> you believe any of it.** `data/policy.gob` is **not** "the single highest-value asset in +> either repo". It is a 10-big-blind push-fold policy, trained against a model of poker +> that is not poker (a call always ends the street), and *it was never once read* — the +> trainer wrote its keys under `IP`/`OOP` and the runtime looked them up under +> `BTN`/`SB`/`BB`, so every lookup in the history of the game missed and fell through to a +> pot-odds heuristic. Nothing about that is visible from the outside: a policy miss is not +> an error. **Retrain against your own engine.** Pete now does — `internal/games/holdem/ +> train.go` plays every move through the real reducer, and both the trainer and the table +> build the info-set key with the same function so they cannot drift apart again. Already mautrix-free, verified by import check: -- `holdem_cfr.go` (1,285) — full CFR trainer + NPC policy runtime, info-set packing into a - `uint64`, regret pruning, board-texture/SPR/equity bucketing. Plus the trained - `data/policy.gob` (3.4 MB) and `cmd/holdem-train`, `cmd/holdem-seed`. **This is the single - highest-value asset in either repo.** +- `holdem_cfr.go` (1,285) — CFR trainer + NPC policy runtime, info-set packing into a + `uint64`, regret pruning, board-texture/SPR/equity bucketing. The *bucketing* is worth + taking (equity, SPR, board texture). The trainer, the tree and the trained policy are + not: see the warning above. - `holdem_equity.go` + `holdem_equity_range.go` (548) — Monte-Carlo equity, equity-vs-range, draw/out detection. 100% pure, well tested. - `holdem_betting.go` (383) — side pots, min-raise, all-in, street completion. The fiddly