The engine has been able to play No Mercy since aca523e. Now a browser can.
The switch is a switch, not a fourth table: the tier is still the table size,
because that is what you are paid for, and the deck is the other dial. Six faces
the normal box does not print, sized by the card's own vars and never by the box
they sit in. The stack says what the bill is on the felt, in the turn line and on
the button, and under it the deck is dead — you cannot draw your way out of a
bill somebody has run up and pointed at you.
The wild draws glow. That started as decoration and turned out to be doing work:
No Mercy prints a coloured +4 right beside the wild one, and in a hand of twenty
the glow is what tells them apart.
A buried seat is not an empty one, which is the whole trap here — a seat killed
at twenty-five holds no cards, and neither does a seat that just went out and
won. The view asks the engine which it is instead of counting to zero, so the
winner is never the corpse.
Two bugs, both found in a browser and neither findable anywhere else:
The felt's stack bill was writing into the chip bar. It was [data-pending], and
so is the bar's "your chips are still coming" readout — and the bar lives inside
the table's own root and comes first in the document. A stack quietly overwrote
the escrow message and never appeared on the felt at all. A table's attributes
are not a private namespace.
And hold'em, re-driven on the 20M-hand policy (six hands, got up 61 ahead of a
100 buy-in, money conserved to the chip — Phase 4 closed), let you click a button
that did nothing: Deal, Leave and Top up stayed alive through the whole deal
animation, where send() drops the click on purpose. The lock is on the buttons
now, not only in the variable.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
213 lines
7.4 KiB
Go
213 lines
7.4 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"pete/internal/games/blackjack"
|
|
"pete/internal/games/hangman"
|
|
"pete/internal/games/holdem"
|
|
"pete/internal/games/klondike"
|
|
"pete/internal/games/trivia"
|
|
"pete/internal/games/uno"
|
|
"pete/internal/storage"
|
|
)
|
|
|
|
// The casino's two pages. Both require a signed-in visitor — there is money in
|
|
// here, and a player has to be somebody gogobee's ledger can name.
|
|
//
|
|
// Neither page renders any game state server-side. The felt is drawn by the
|
|
// browser from /api/games/table, because a hand is a thing that *happens*: cards
|
|
// are dealt one at a time and the table plays that back. A server-rendered hand
|
|
// would arrive fully formed, which is the one thing a card table must never do.
|
|
|
|
// gameTeaser is a table that isn't open yet. They're on the lobby because an
|
|
// empty casino with one game reads as broken, and this reads as early.
|
|
type gameTeaser struct {
|
|
Name string
|
|
Emoji string
|
|
Blurb string
|
|
}
|
|
|
|
// 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}
|
|
|
|
// The casino is not called Pete — the news app is Pete's, and this is somewhere
|
|
// you go. It has two names, and which one is over the door depends on the hour:
|
|
// the lights come on at six and the place turns into Casino Night Zone until
|
|
// dawn. Same tables, different room.
|
|
type room struct {
|
|
Slug string // drives the palette: html[data-room="…"]
|
|
Name string // what's on the sign
|
|
}
|
|
|
|
var (
|
|
roomDay = room{Slug: "casinopolis", Name: "Casinopolis"}
|
|
roomNight = room{Slug: "casino-night", Name: "Casino Night Zone"}
|
|
)
|
|
|
|
// roomAt picks the room for an hour of the day. Daylight is 6am to 6pm; the rest
|
|
// belongs to the neon. The browser re-runs this same rule against its own clock
|
|
// (games_layout.html), so a player in another timezone sees their own evening —
|
|
// this server-side pick only exists so the first paint isn't the wrong room.
|
|
func roomAt(hour int) room {
|
|
if hour >= 6 && hour < 18 {
|
|
return roomDay
|
|
}
|
|
return roomNight
|
|
}
|
|
|
|
// gamesPage is deliberately *not* pageData. The casino shares Pete's design
|
|
// language and nothing else — no channels, no weather, no sources, no push.
|
|
// Giving it its own page struct is what stops the news app's furniture drifting
|
|
// back in one convenient field at a time.
|
|
type gamesPage struct {
|
|
Room room
|
|
User *SessionUser
|
|
Cap int64
|
|
RakePct int
|
|
Soon []gameTeaser
|
|
Denominations []int64
|
|
Tiers []hangman.Tier // hangman's three lengths, and what each pays
|
|
MaxWrong int
|
|
Deals []klondike.Tier // solitaire's three deals
|
|
FullDeck int
|
|
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
|
|
NoMercy []uno.Tier // the same three, playing the other rules
|
|
MercyLimit int // the hand that ends you in No Mercy
|
|
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.
|
|
//
|
|
// It exists so there is exactly one list of them. The dev rig (devcasino_test.go)
|
|
// has to wire its own mux — New() decides whether the casino exists before the
|
|
// rig has signed anybody in — and a second copy of this list is a list that
|
|
// silently stops including the newest game.
|
|
func (s *Server) casinoRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /games", s.handleLobby)
|
|
mux.HandleFunc("GET /games/blackjack", s.handleBlackjack)
|
|
mux.HandleFunc("GET /games/hangman", s.handleHangman)
|
|
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)
|
|
mux.HandleFunc("POST /api/games/cashout", s.handleCashOut)
|
|
|
|
mux.HandleFunc("POST /api/games/blackjack/deal", s.handleDeal)
|
|
mux.HandleFunc("POST /api/games/blackjack/move", s.handleMove)
|
|
|
|
mux.HandleFunc("POST /api/games/hangman/start", s.handleHangmanStart)
|
|
mux.HandleFunc("POST /api/games/hangman/guess", s.handleHangmanGuess)
|
|
|
|
mux.HandleFunc("POST /api/games/solitaire/start", s.handleSolitaireStart)
|
|
mux.HandleFunc("POST /api/games/solitaire/move", s.handleSolitaireMove)
|
|
|
|
mux.HandleFunc("POST /api/games/trivia/start", s.handleTriviaStart)
|
|
mux.HandleFunc("POST /api/games/trivia/answer", s.handleTriviaAnswer)
|
|
|
|
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.
|
|
// Anyone who is signed in but carries a session from before the casino existed
|
|
// has no username in it, so they get sent through sign-in too — which mints one.
|
|
func (s *Server) requirePlayer(w http.ResponseWriter, r *http.Request) bool {
|
|
if !s.gamesReady() {
|
|
http.NotFound(w, r)
|
|
return false
|
|
}
|
|
u := s.auth.userFromRequest(r)
|
|
if u != nil && u.MatrixUser(s.cfg.Games.MatrixServer) != "" {
|
|
return true
|
|
}
|
|
http.Redirect(w, r, "/auth/login?next="+r.URL.Path, http.StatusFound)
|
|
return false
|
|
}
|
|
|
|
func (s *Server) gamesPage(r *http.Request) gamesPage {
|
|
return gamesPage{
|
|
Room: roomAt(time.Now().Hour()),
|
|
User: s.auth.userFromRequest(r), // requirePlayer ran first, so this is non-nil
|
|
Cap: storage.MaxChipsOnTable,
|
|
RakePct: int(blackjack.DefaultRules().RakePct * 100),
|
|
Soon: comingSoon,
|
|
Denominations: betDenominations,
|
|
Tiers: hangman.Tiers,
|
|
MaxWrong: hangman.MaxWrong,
|
|
Deals: klondike.Tiers,
|
|
FullDeck: klondike.FullDeck,
|
|
Quizzes: trivia.Tiers,
|
|
Rungs: trivia.Rungs,
|
|
Tables: uno.Tiers,
|
|
NoMercy: uno.NoMercyTiers,
|
|
MercyLimit: uno.MercyLimit,
|
|
Stakes: holdem.Tiers,
|
|
MaxBots: holdem.MaxBots,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleLobby(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
s.render(w, "games", s.gamesPage(r))
|
|
}
|
|
|
|
func (s *Server) handleBlackjack(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
s.render(w, "blackjack", s.gamesPage(r))
|
|
}
|
|
|
|
func (s *Server) handleHangman(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
s.render(w, "hangman", s.gamesPage(r))
|
|
}
|
|
|
|
func (s *Server) handleSolitaire(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
s.render(w, "solitaire", s.gamesPage(r))
|
|
}
|
|
|
|
func (s *Server) handleTrivia(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
s.render(w, "trivia", s.gamesPage(r))
|
|
}
|
|
|
|
func (s *Server) handleUno(w http.ResponseWriter, r *http.Request) {
|
|
if !s.requirePlayer(w, r) {
|
|
return
|
|
}
|
|
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))
|
|
}
|