package web import ( "net/http" "time" "pete/internal/games/blackjack" "pete/internal/games/hangman" "pete/internal/games/klondike" "pete/internal/games/trivia" "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."}, } // 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 } // 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 /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) } // 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, } } 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)) }