Files
Pete/internal/web/games_pages.go
prosolis 8ec13eab5b games: the casino moves out, and gets a clock of its own
The tables were living in the news app's shell: Pete's face in the header
and the footer, the channel nav, search, the reader, the weather canvas,
the PWA. A casino is not a news page with a felt on it.

So it gets its own layout. What carries over is the design language — the
four palette vars, Fredoka/Nunito, the fat rounded cards, the dropped
shadow. What doesn't is every control it has no use for. gamesPage stops
embedding the news pageData, which is what keeps the furniture from
drifting back one convenient field at a time.

It keeps a clock, but tells a different joke with it: Casinopolis by day,
Casino Night Zone from six, palette and felt and the sign over the door all
changing together. The rule lives in roomAt() for the first paint and again
in the browser, so a player abroad gets their own evening.

And the cards are cards now — corner indices in both corners, the bottom
one upside down as printed, pips on the three-by-seven grid every real deck
has used for four hundred years, courts as a letter with the suit over each
shoulder. Driven in a real browser, both rooms, dealt through to a payout.
2026-07-13 23:40:33 -07:00

115 lines
3.9 KiB
Go

package web
import (
"net/http"
"time"
"pete/internal/games/blackjack"
"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
}
var comingSoon = []gameTeaser{
{Name: "Hold'em", Emoji: "♠️", Blurb: "Six seats, and the house bots know how to play."},
{Name: "UNO", Emoji: "🎴", Blurb: "Normal rules, or no mercy."},
{Name: "Trivia", Emoji: "🧠", Blurb: "Faster answers score higher."},
{Name: "Hangman", Emoji: "🪢", Blurb: "Guess the phrase before the gallows finish."},
}
// 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
}
// 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,
}
}
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))
}