Define KeyPlayerID, KeyUserID, and KeyNickname in the sessions package and use them across all handlers to avoid duplicated magic strings.
308 lines
7.6 KiB
Go
308 lines
7.6 KiB
Go
package c4game
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/alexedwards/scs/v2"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/starfederation/datastar-go/datastar"
|
|
|
|
"github.com/ryanhamamura/games/chat"
|
|
chatcomponents "github.com/ryanhamamura/games/chat/components"
|
|
"github.com/ryanhamamura/games/connect4"
|
|
"github.com/ryanhamamura/games/db/repository"
|
|
"github.com/ryanhamamura/games/features/c4game/pages"
|
|
"github.com/ryanhamamura/games/sessions"
|
|
)
|
|
|
|
// c4ChatColors maps player color (1=Red, 2=Yellow) to CSS background colors.
|
|
var c4ChatColors = map[int]string{
|
|
0: "#4a2a3a", // color 1 stored as slot 0
|
|
1: "#2a4545", // color 2 stored as slot 1
|
|
}
|
|
|
|
func c4ChatColor(slot int) string {
|
|
if c, ok := c4ChatColors[slot]; ok {
|
|
return c
|
|
}
|
|
return "#666"
|
|
}
|
|
|
|
func c4ChatConfig(gameID string) chatcomponents.Config {
|
|
return chatcomponents.Config{
|
|
CSSPrefix: "c4",
|
|
PostURL: fmt.Sprintf("/games/%s/chat", gameID),
|
|
Color: c4ChatColor,
|
|
}
|
|
}
|
|
|
|
func HandleGamePage(store *connect4.Store, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
playerID := sessions.GetPlayerID(sm, r)
|
|
userID := sessions.GetUserID(sm, r)
|
|
nickname := sessions.GetNickname(sm, r)
|
|
|
|
// Auto-join if player has a nickname but isn't in the game yet
|
|
if nickname != "" && gi.GetPlayerColor(playerID) == 0 {
|
|
p := &connect4.Player{
|
|
ID: playerID,
|
|
Nickname: nickname,
|
|
}
|
|
if userID != "" {
|
|
p.UserID = &userID
|
|
}
|
|
gi.Join(&connect4.PlayerSession{Player: p})
|
|
}
|
|
|
|
myColor := gi.GetPlayerColor(playerID)
|
|
|
|
if myColor == 0 {
|
|
// Player not in game
|
|
isGuest := r.URL.Query().Get("guest") == "1"
|
|
if userID == "" && !isGuest {
|
|
if err := pages.JoinPage(gameID).Render(r.Context(), w); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
if err := pages.NicknamePage(gameID).Render(r.Context(), w); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
g := gi.GetGame()
|
|
room := chat.NewPersistentRoom(nil, "", queries, gameID)
|
|
|
|
if err := pages.GamePage(g, myColor, room.Messages(), c4ChatConfig(gameID)).Render(r.Context(), w); err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
http.Error(w, "game not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
playerID := sessions.GetPlayerID(sm, r)
|
|
myColor := gi.GetPlayerColor(playerID)
|
|
|
|
sse := datastar.NewSSE(w, r, datastar.WithCompression(
|
|
datastar.WithBrotli(datastar.WithBrotliLevel(5)),
|
|
))
|
|
|
|
chatCfg := c4ChatConfig(gameID)
|
|
room := chat.NewPersistentRoom(nc, connect4.ChatSubject(gameID), queries, gameID)
|
|
|
|
patchAll := func() error {
|
|
myColor = gi.GetPlayerColor(playerID)
|
|
g := gi.GetGame()
|
|
return sse.PatchElementTempl(pages.GameContent(g, myColor, room.Messages(), chatCfg))
|
|
}
|
|
|
|
// Send initial render
|
|
if err := patchAll(); err != nil {
|
|
return
|
|
}
|
|
|
|
// Subscribe to game state updates
|
|
gameCh := make(chan *nats.Msg, 64)
|
|
gameSub, err := nc.ChanSubscribe(connect4.GameSubject(gameID), gameCh)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer gameSub.Unsubscribe() //nolint:errcheck
|
|
|
|
// Subscribe to chat messages
|
|
chatCh, chatSub, err := room.Subscribe()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer chatSub.Unsubscribe() //nolint:errcheck
|
|
|
|
ctx := r.Context()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-gameCh:
|
|
if err := patchAll(); err != nil {
|
|
return
|
|
}
|
|
case msg := <-chatCh:
|
|
room.Receive(msg.Data)
|
|
if err := patchAll(); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func HandleDropPiece(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
http.Error(w, "game not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
colStr := r.URL.Query().Get("col")
|
|
col, err := strconv.Atoi(colStr)
|
|
if err != nil {
|
|
http.Error(w, "invalid column", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
playerID := sessions.GetPlayerID(sm, r)
|
|
myColor := gi.GetPlayerColor(playerID)
|
|
if myColor == 0 {
|
|
http.Error(w, "not in game", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
gi.DropPiece(col, myColor)
|
|
datastar.NewSSE(w, r)
|
|
}
|
|
}
|
|
|
|
func HandleSendChat(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
http.Error(w, "game not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
type ChatSignals struct {
|
|
ChatMsg string `json:"chatMsg"`
|
|
}
|
|
var signals ChatSignals
|
|
if err := datastar.ReadSignals(r, &signals); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if signals.ChatMsg == "" {
|
|
datastar.NewSSE(w, r)
|
|
return
|
|
}
|
|
|
|
playerID := sessions.GetPlayerID(sm, r)
|
|
myColor := gi.GetPlayerColor(playerID)
|
|
if myColor == 0 {
|
|
datastar.NewSSE(w, r)
|
|
return
|
|
}
|
|
|
|
g := gi.GetGame()
|
|
nick := ""
|
|
for _, p := range g.Players {
|
|
if p != nil && p.ID == playerID {
|
|
nick = p.Nickname
|
|
break
|
|
}
|
|
}
|
|
|
|
// Map color (1-based) to slot (0-based) for the unified chat message
|
|
msg := chat.Message{
|
|
Nickname: nick,
|
|
Slot: myColor - 1,
|
|
Message: signals.ChatMsg,
|
|
Time: time.Now().UnixMilli(),
|
|
}
|
|
room := chat.NewPersistentRoom(nc, connect4.ChatSubject(gameID), queries, gameID)
|
|
room.Send(msg)
|
|
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.MarshalAndPatchSignals(map[string]any{"chatMsg": ""}) //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
func HandleSetNickname(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.Redirect("/") //nolint:errcheck
|
|
return
|
|
}
|
|
|
|
type NicknameSignals struct {
|
|
Nickname string `json:"nickname"`
|
|
}
|
|
var signals NicknameSignals
|
|
if err := datastar.ReadSignals(r, &signals); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if signals.Nickname == "" {
|
|
datastar.NewSSE(w, r)
|
|
return
|
|
}
|
|
|
|
sm.Put(r.Context(), sessions.KeyNickname, signals.Nickname)
|
|
|
|
playerID := sessions.GetPlayerID(sm, r)
|
|
userID := sessions.GetUserID(sm, r)
|
|
|
|
if gi.GetPlayerColor(playerID) == 0 {
|
|
p := &connect4.Player{
|
|
ID: playerID,
|
|
Nickname: signals.Nickname,
|
|
}
|
|
if userID != "" {
|
|
p.UserID = &userID
|
|
}
|
|
gi.Join(&connect4.PlayerSession{Player: p})
|
|
}
|
|
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.Redirect("/games/" + gameID) //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
func HandleRematch(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
gameID := chi.URLParam(r, "id")
|
|
|
|
gi, exists := store.Get(gameID)
|
|
if !exists {
|
|
sse := datastar.NewSSE(w, r)
|
|
sse.Redirect("/") //nolint:errcheck
|
|
return
|
|
}
|
|
|
|
newGI := gi.CreateRematch(store)
|
|
sse := datastar.NewSSE(w, r)
|
|
if newGI != nil {
|
|
sse.Redirectf("/games/%s", newGI.ID()) //nolint:errcheck
|
|
}
|
|
}
|
|
}
|