From: Skullheadx Date: Mon, 8 Jun 2026 02:00:47 +0000 (-0400) Subject: lobby backend X-Git-Url: http://git.skullheadx.com/index.js?a=commitdiff_plain;h=89c546c23f816a778ff98d3d359b5490fbca719f;p=monopoly-web.git lobby backend --- diff --git a/game/config.go b/game/config.go index 5644f4f..b83175b 100644 --- a/game/config.go +++ b/game/config.go @@ -1,5 +1,10 @@ package game +const ( + MSG_START uint8 = iota + MSG_END +) + const ( StartingDiceRolls int32 = 1 ) diff --git a/game/game.go b/game/game.go index 56a0502..1da75b3 100644 --- a/game/game.go +++ b/game/game.go @@ -4,6 +4,7 @@ import ( "context" "errors" "github.com/coder/websocket" + "github.com/google/uuid" "golang.org/x/time/rate" "io" "log" @@ -14,6 +15,189 @@ import ( "time" ) +type MonopolyServer struct { + subscriberMessageBuffer int + publishLimiter *rate.Limiter + + logf func(f string, v ...any) + + serveMux http.ServeMux + + subscribersMu sync.Mutex + subscribers map[*subscriber]string + + gameCtxMu sync.Mutex + gameCtx *Context + randSeed *rand.PCG +} + +func NewMonopolyServer() *MonopolyServer { + ms := &MonopolyServer{ + subscriberMessageBuffer: 16, + logf: log.Printf, + subscribers: make(map[*subscriber]string), + publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8), + gameCtx: nil, + randSeed: rand.NewPCG(20, 26), + } + ms.serveMux.Handle("/", http.FileServer(http.Dir("../public/"))) + ms.serveMux.HandleFunc("/subscribe", ms.subscribeHandler) + ms.serveMux.HandleFunc("/publish", ms.publishHandler) + + return ms +} + +type subscriber struct { + msgs chan []byte + closeSlow func() +} + +func (ms *MonopolyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ms.serveMux.ServeHTTP(w, r) +} + +func (ms *MonopolyServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { + err := ms.subscribe(w, r) + if errors.Is(err, context.Canceled) { + return + } + + if websocket.CloseStatus(err) == websocket.StatusNormalClosure || websocket.CloseStatus(err) == websocket.StatusGoingAway { + return + } + + if err != nil { + ms.logf("%v", err) + return + } +} + +func (ms *MonopolyServer) publishHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + body := http.MaxBytesReader(w, r.Body, 8192) + msg, err := io.ReadAll(body) + if err != nil { + http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) + return + } + + ms.publish(msg) + w.WriteHeader(http.StatusAccepted) +} + +func (ms *MonopolyServer) subscribe(w http.ResponseWriter, r *http.Request) error { + var mu sync.Mutex + var c *websocket.Conn + var closed bool + + s := &subscriber{ + msgs: make(chan []byte, ms.subscriberMessageBuffer), + closeSlow: func() { + mu.Lock() + defer mu.Unlock() + closed = true + if c != nil { + c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + } + }, + } + ms.addSubscriber(s, uuid.NewString()) + defer ms.deleteSubscriber(s) + + c2, err := websocket.Accept(w, r, nil) + if err != nil { + return err + } + + mu.Lock() + if closed { + mu.Unlock() + return net.ErrClosed + } + + c = c2 + mu.Unlock() + defer c.CloseNow() + + ctx := c.CloseRead(context.Background()) + for { + select { + case msg := <-s.msgs: + err := writeTimeout(ctx, time.Second*5, c, msg) + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (ms *MonopolyServer) startGame() { + ms.subscribersMu.Lock() + defer ms.subscribersMu.Unlock() + + ms.publishLimiter.Wait(context.Background()) + + var players []Player + for _, uuid := range ms.subscribers { + players = append(players, InitPlayer(uuid)) + } + + ms.gameCtxMu.Lock() + ms.gameCtx = InitCtx(ms.randSeed, players) + ms.gameCtxMu.Unlock() + + msg := []byte{MSG_START} + for s := range ms.subscribers { + select { + case s.msgs <- msg: + default: + go s.closeSlow() + } + } + +} + +func (ms *MonopolyServer) publish(msg []byte) { + ms.subscribersMu.Lock() + defer ms.subscribersMu.Unlock() + + ms.publishLimiter.Wait(context.Background()) + + for s := range ms.subscribers { + select { + case s.msgs <- msg: + default: + go s.closeSlow() + } + } + +} + +func (ms *MonopolyServer) addSubscriber(s *subscriber, uuid string) { + ms.subscribersMu.Lock() + ms.subscribers[s] = uuid + ms.subscribersMu.Unlock() +} + +func (ms *MonopolyServer) deleteSubscriber(s *subscriber) { + ms.subscribersMu.Lock() + delete(ms.subscribers, s) + ms.subscribersMu.Unlock() +} + +func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return c.Write(ctx, websocket.MessageText, msg) +} + func initTurn(pID PlayerID, inJail bool) Turn { eS := []EventType{EventEndTurn} @@ -87,9 +271,9 @@ func InitCtx(randSeed rand.Source, players []Player) *Context { } } -func InitPlayer() Player { +func InitPlayer(uuid string) Player { return Player{ - UUID: "abc", // TODO: Generate proper UUID + UUID: uuid, Money: StartingMoney, CurrentSpaceID: SpecialSpaces.Go, GetOutOfJailCards: StartingGetOutOfJailFreeCards, @@ -214,153 +398,3 @@ func (ctx *Context) getPropID(spaceID SpaceID) PropertyID { } panic("Space is not an ownable property") } - -type MonopolyServer struct { - subscriberMessageBuffer int - publishLimiter *rate.Limiter - - logf func(f string, v ...any) - - serveMux http.ServeMux - - subscribersMu sync.Mutex - subscribers map[*subscriber]struct{} -} - -func NewMonopolyServer() *MonopolyServer { - ms := &MonopolyServer{ - subscriberMessageBuffer: 16, - logf: log.Printf, - subscribers: make(map[*subscriber]struct{}), - publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8), - } - ms.serveMux.Handle("/", http.FileServer(http.Dir("../public/"))) - ms.serveMux.HandleFunc("/subscribe", ms.subscribeHandler) - ms.serveMux.HandleFunc("/publish", ms.publishHandler) - - return ms -} - -type subscriber struct { - msgs chan []byte - closeSlow func() -} - -func (ms *MonopolyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ms.serveMux.ServeHTTP(w, r) -} - -func (ms *MonopolyServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - err := ms.subscribe(w, r) - if errors.Is(err, context.Canceled) { - return - } - - if websocket.CloseStatus(err) == websocket.StatusNormalClosure || websocket.CloseStatus(err) == websocket.StatusGoingAway { - return - } - - if err != nil { - ms.logf("%v", err) - return - } -} - -func (ms *MonopolyServer) publishHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - return - } - - body := http.MaxBytesReader(w, r.Body, 8192) - msg, err := io.ReadAll(body) - if err != nil { - http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) - return - } - - ms.publish(msg) - w.WriteHeader(http.StatusAccepted) -} - -func (ms *MonopolyServer) subscribe(w http.ResponseWriter, r *http.Request) error { - var mu sync.Mutex - var c *websocket.Conn - var closed bool - s := &subscriber{ - msgs: make(chan []byte, ms.subscriberMessageBuffer), - closeSlow: func() { - mu.Lock() - defer mu.Unlock() - closed = true - if c != nil { - c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") - } - }, - } - ms.addSubscriber(s) - defer ms.deleteSubscriber(s) - - c2, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - - mu.Lock() - if closed { - mu.Unlock() - return net.ErrClosed - } - - c = c2 - mu.Unlock() - defer c.CloseNow() - - ctx := c.CloseRead(context.Background()) - for { - select { - case msg := <-s.msgs: - err := writeTimeout(ctx, time.Second*5, c, msg) - if err != nil { - return err - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - -func (ms *MonopolyServer) publish(msg []byte) { - ms.subscribersMu.Lock() - defer ms.subscribersMu.Unlock() - - ms.publishLimiter.Wait(context.Background()) - - for s := range ms.subscribers { - select { - case s.msgs <- msg: - default: - go s.closeSlow() - } - } - -} - -func (ms *MonopolyServer) addSubscriber(s *subscriber) { - ms.subscribersMu.Lock() - ms.subscribers[s] = struct{}{} - ms.subscribersMu.Unlock() -} - -func (ms *MonopolyServer) deleteSubscriber(s *subscriber) { - ms.subscribersMu.Lock() - delete(ms.subscribers, s) - ms.subscribersMu.Unlock() -} - -func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - return c.Write(ctx, websocket.MessageText, msg) -} diff --git a/go.mod b/go.mod index f5d43a3..f178e3e 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,5 @@ require ( github.com/coder/websocket v1.8.14 golang.org/x/time v0.15.0 ) + +require github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index aa6ad6f..dfd6467 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/main.go b/main.go index a1f05b6..dbc722c 100644 --- a/main.go +++ b/main.go @@ -29,25 +29,6 @@ func initRoom() Room { } } -// func main() { -// fmt.Println("monopoly-web backend") -// -// room := initRoom() -// -// // register routes -// http.HandleFunc("/health", healthHandler) -// http.HandleFunc("/api/v1/roll", room.rollDiceHandler) -// http.HandleFunc("POST /api/v1/turn", room.endTurnHandler) -// http.HandleFunc("POST /api/v1/exit-jail", room.exitJailHandler) -// -// // listen and serve -// log.Fatal(http.ListenAndServe(":8080", nil)) -// } - -func healthHandler(w http.ResponseWriter, req *http.Request) { - io.WriteString(w, "Status: healthy\n") -} - const UUID = "abc" // TODO: UUID in cookie func (r *Room) rollDiceHandler(w http.ResponseWriter, req *http.Request) { @@ -115,7 +96,7 @@ func run() error { } log.Printf("listening on ws://%v", l.Addr()) - cs := newMonopolyServer() + cs := game.NewMonopolyServer() s := &http.Server{ Handler: cs, ReadTimeout: time.Second * 10,