From 22ddf340f0b0c8d0cd04c34d9fa1481a1fbf422f Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 28 Jul 2024 19:42:05 +0900 Subject: refactor(backend): move game-related code to game module --- backend/game.go | 328 ----------------------------------------- backend/game/game.go | 385 ++++++++++++++++++++++++++++++++++++++++++++++++ backend/game/http.go | 56 +++++++ backend/game/message.go | 104 +++++++++++++ backend/game/ws.go | 46 ++++++ backend/main.go | 131 +++------------- backend/message.go | 104 ------------- 7 files changed, 610 insertions(+), 544 deletions(-) delete mode 100644 backend/game.go create mode 100644 backend/game/game.go create mode 100644 backend/game/http.go create mode 100644 backend/game/message.go create mode 100644 backend/game/ws.go delete mode 100644 backend/message.go (limited to 'backend') diff --git a/backend/game.go b/backend/game.go deleted file mode 100644 index a8f688e..0000000 --- a/backend/game.go +++ /dev/null @@ -1,328 +0,0 @@ -package main - -import ( - "log" - "net/http" - "time" - - "github.com/gorilla/websocket" -) - -type GameHub struct { - game *Game - clients map[*GameClient]bool - receive chan *MessageWithClient - register chan *GameClient - unregister chan *GameClient - watchers map[*GameWatcher]bool - registerWatcher chan *GameWatcher - unregisterWatcher chan *GameWatcher - state int - finishTime time.Time -} - -func NewGameHub(game *Game) *GameHub { - return &GameHub{ - game: game, - clients: make(map[*GameClient]bool), - receive: make(chan *MessageWithClient), - register: make(chan *GameClient), - unregister: make(chan *GameClient), - watchers: make(map[*GameWatcher]bool), - registerWatcher: make(chan *GameWatcher), - unregisterWatcher: make(chan *GameWatcher), - state: 0, - } -} - -func (h *GameHub) Run() { - ticker := time.NewTicker(10 * time.Second) - defer func() { - ticker.Stop() - }() - - for { - select { - case client := <-h.register: - h.clients[client] = true - log.Printf("client registered: %d", len(h.clients)) - case client := <-h.unregister: - if _, ok := h.clients[client]; ok { - h.closeClient(client) - } - log.Printf("client unregistered: %d", len(h.clients)) - if len(h.clients) == 0 { - h.Close() - return - } - case watcher := <-h.registerWatcher: - h.watchers[watcher] = true - log.Printf("watcher registered: %d", len(h.watchers)) - case watcher := <-h.unregisterWatcher: - if _, ok := h.watchers[watcher]; ok { - h.closeWatcher(watcher) - } - log.Printf("watcher unregistered: %d", len(h.watchers)) - case message := <-h.receive: - log.Printf("received message: %s", message.Message.Type) - switch message.Message.Type { - case "connect": - if h.state == 0 { - h.state = 1 - } else if h.state == 1 { - h.state = 2 - for client := range h.clients { - client.send <- &Message{Type: "prepare", Data: MessageDataPrepare{Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。"}} - } - } else { - log.Printf("invalid state: %d", h.state) - h.closeClient(message.Client) - } - case "ready": - if h.state == 2 { - h.state = 3 - } else if h.state == 3 { - h.state = 4 - for client := range h.clients { - client.send <- &Message{Type: "start", Data: MessageDataStart{StartTime: time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339)}} - } - h.finishTime = time.Now().Add(3 * time.Minute) - } else { - log.Printf("invalid state: %d", h.state) - h.closeClient(message.Client) - } - case "code": - if h.state == 4 { - code := message.Message.Data.(MessageDataCode).Code - message.Client.code = code - message.Client.send <- &Message{Type: "score", Data: MessageDataScore{Score: 100}} - if message.Client.score == nil { - message.Client.score = new(int) - } - *message.Client.score = 100 - - var scoreA, scoreB *int - var codeA, codeB string - for client := range h.clients { - if client.team == "a" { - scoreA = client.score - codeA = client.code - } else { - scoreB = client.score - codeB = client.code - } - } - for watcher := range h.watchers { - watcher.send <- &Message{ - Type: "watch", - Data: MessageDataWatch{ - Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。", - ScoreA: scoreA, - CodeA: codeA, - ScoreB: scoreB, - CodeB: codeB, - }, - } - } - } else { - log.Printf("invalid state: %d", h.state) - h.closeClient(message.Client) - } - default: - log.Printf("unknown message type: %s", message.Message.Type) - h.closeClient(message.Client) - } - case <-ticker.C: - log.Printf("state: %d", h.state) - if h.state == 4 { - if time.Now().After(h.finishTime) { - h.state = 5 - clientAndScores := make(map[*GameClient]*int) - for client := range h.clients { - clientAndScores[client] = client.score - } - for client, score := range clientAndScores { - var opponentScore *int - for c2, s2 := range clientAndScores { - if c2 != client { - opponentScore = s2 - break - } - } - client.send <- &Message{Type: "finish", Data: MessageDataFinish{YourScore: score, OpponentScore: opponentScore}} - } - } - } - } - } -} - -func (h *GameHub) Close() { - for client := range h.clients { - h.closeClient(client) - } - close(h.receive) - close(h.register) - close(h.unregister) - for watcher := range h.watchers { - h.closeWatcher(watcher) - } - close(h.registerWatcher) - close(h.unregisterWatcher) -} - -func (h *GameHub) closeClient(client *GameClient) { - delete(h.clients, client) - close(client.send) -} - -func (h *GameHub) closeWatcher(watcher *GameWatcher) { - delete(h.watchers, watcher) - close(watcher.send) -} - -const ( - writeWait = 10 * time.Second - pongWait = 60 * time.Second - pingPeriod = (pongWait * 9) / 10 - maxMessageSize = 512 -) - -var ( - newline = []byte{'\n'} - space = []byte{' '} -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -type GameClient struct { - hub *GameHub - conn *websocket.Conn - send chan *Message - score *int - code string - team string -} - -type GameWatcher struct { - hub *GameHub - conn *websocket.Conn - send chan *Message -} - -// Receives messages from the client and sends them to the hub. -func (c *GameClient) readPump() { - defer func() { - log.Printf("closing client") - c.hub.unregister <- c - c.conn.Close() - }() - c.conn.SetReadLimit(maxMessageSize) - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) - for { - var message Message - err := c.conn.ReadJSON(&message) - if err != nil { - log.Printf("error: %v", err) - return - } - c.hub.receive <- &MessageWithClient{c, &message} - } -} - -// Receives messages from the hub and sends them to the client. -func (c *GameClient) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - c.conn.Close() - }() - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if !ok { - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - err := c.conn.WriteJSON(message) - if err != nil { - return - } - case <-ticker.C: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} - -func serveWs(hub *GameHub, w http.ResponseWriter, r *http.Request, team string) error { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return err - } - client := &GameClient{hub: hub, conn: conn, send: make(chan *Message), team: team} - client.hub.register <- client - - go client.writePump() - go client.readPump() - return nil -} - -func serveWsWatcher(hub *GameHub, w http.ResponseWriter, r *http.Request) error { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return err - } - watcher := &GameWatcher{hub: hub, conn: conn, send: make(chan *Message)} - watcher.hub.registerWatcher <- watcher - - go watcher.writePump() - go watcher.readPump() - return nil -} - -// Receives messages from the client and sends them to the hub. -func (c *GameWatcher) readPump() { - c.conn.SetReadLimit(maxMessageSize) - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) -} - -// Receives messages from the hub and sends them to the client. -func (c *GameWatcher) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - c.conn.Close() - log.Printf("closing watcher") - c.hub.unregisterWatcher <- c - }() - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if !ok { - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - err := c.conn.WriteJSON(message) - if err != nil { - return - } - case <-ticker.C: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} diff --git a/backend/game/game.go b/backend/game/game.go new file mode 100644 index 0000000..9e63a1e --- /dev/null +++ b/backend/game/game.go @@ -0,0 +1,385 @@ +package game + +import ( + "context" + "log" + "time" + + "github.com/gorilla/websocket" + + "github.com/nsfisis/iosdc-2024-albatross/backend/api" + "github.com/nsfisis/iosdc-2024-albatross/backend/db" +) + +type gameState = api.GameState + +const ( + gameStateClosed gameState = api.Closed + gameStateWaitingEntries gameState = api.WaitingEntries + gameStateWaitingStart gameState = api.WaitingStart + gameStatePrepare gameState = api.Prepare + gameStateStarting gameState = api.Starting + gameStateGaming gameState = api.Gaming + gameStateFinished gameState = api.Finished +) + +type game struct { + gameID int + state string + displayName string + durationSeconds int + startedAt *time.Time + problem *problem +} + +type problem struct { + problemID int + title string + description string +} + +// func startGame(game *Game) { +// if gameHubs[game.GameID] != nil { +// return +// } +// gameHubs[game.GameID] = NewGameHub(game) +// go gameHubs[game.GameID].Run() +// } + +/* +func handleGolfPost(w http.ResponseWriter, r *http.Request) { + var yourTeam string + waitingGolfGames := []Game{} + err := db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting) + if err != nil { + http.Error(w, "Error getting games", http.StatusInternalServerError) + return + } + if len(waitingGolfGames) == 0 { + _, err = db.Exec("INSERT INTO games (type, state) VALUES ($1, $2)", gameTypeGolf, gameStateWaiting) + if err != nil { + http.Error(w, "Error creating game", http.StatusInternalServerError) + return + } + waitingGolfGames = []Game{} + err = db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting) + if err != nil { + http.Error(w, "Error getting games", http.StatusInternalServerError) + return + } + yourTeam = "a" + startGame(&waitingGolfGames[0]) + } else { + yourTeam = "b" + db.Exec("UPDATE games SET state = $1 WHERE game_id = $2", gameStateReady, waitingGolfGames[0].GameID) + } + waitingGame := waitingGolfGames[0] + + http.Redirect(w, r, fmt.Sprintf("/golf/%d/%s/", waitingGame.GameID, yourTeam), http.StatusSeeOther) +} +*/ + +type GameHub struct { + game *game + clients map[*GameClient]bool + receive chan *MessageWithClient + register chan *GameClient + unregister chan *GameClient + watchers map[*GameWatcher]bool + registerWatcher chan *GameWatcher + unregisterWatcher chan *GameWatcher + state int + finishTime time.Time +} + +func NewGameHub(game *game) *GameHub { + return &GameHub{ + game: game, + clients: make(map[*GameClient]bool), + receive: make(chan *MessageWithClient), + register: make(chan *GameClient), + unregister: make(chan *GameClient), + watchers: make(map[*GameWatcher]bool), + registerWatcher: make(chan *GameWatcher), + unregisterWatcher: make(chan *GameWatcher), + state: 0, + } +} + +func (h *GameHub) Run() { + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + }() + + for { + select { + case client := <-h.register: + h.clients[client] = true + log.Printf("client registered: %d", len(h.clients)) + case client := <-h.unregister: + if _, ok := h.clients[client]; ok { + h.closeClient(client) + } + log.Printf("client unregistered: %d", len(h.clients)) + if len(h.clients) == 0 { + h.Close() + return + } + case watcher := <-h.registerWatcher: + h.watchers[watcher] = true + log.Printf("watcher registered: %d", len(h.watchers)) + case watcher := <-h.unregisterWatcher: + if _, ok := h.watchers[watcher]; ok { + h.closeWatcher(watcher) + } + log.Printf("watcher unregistered: %d", len(h.watchers)) + case message := <-h.receive: + log.Printf("received message: %s", message.Message.Type) + switch message.Message.Type { + case "connect": + if h.state == 0 { + h.state = 1 + } else if h.state == 1 { + h.state = 2 + for client := range h.clients { + client.send <- &Message{Type: "prepare", Data: MessageDataPrepare{Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。"}} + } + } else { + log.Printf("invalid state: %d", h.state) + h.closeClient(message.Client) + } + case "ready": + if h.state == 2 { + h.state = 3 + } else if h.state == 3 { + h.state = 4 + for client := range h.clients { + client.send <- &Message{Type: "start", Data: MessageDataStart{StartTime: time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339)}} + } + h.finishTime = time.Now().Add(3 * time.Minute) + } else { + log.Printf("invalid state: %d", h.state) + h.closeClient(message.Client) + } + case "code": + if h.state == 4 { + code := message.Message.Data.(MessageDataCode).Code + message.Client.code = code + message.Client.send <- &Message{Type: "score", Data: MessageDataScore{Score: 100}} + if message.Client.score == nil { + message.Client.score = new(int) + } + *message.Client.score = 100 + + var scoreA, scoreB *int + var codeA, codeB string + for client := range h.clients { + if client.team == "a" { + scoreA = client.score + codeA = client.code + } else { + scoreB = client.score + codeB = client.code + } + } + for watcher := range h.watchers { + watcher.send <- &Message{ + Type: "watch", + Data: MessageDataWatch{ + Problem: "1 から 100 までの FizzBuzz を実装せよ (終端を含む)。", + ScoreA: scoreA, + CodeA: codeA, + ScoreB: scoreB, + CodeB: codeB, + }, + } + } + } else { + log.Printf("invalid state: %d", h.state) + h.closeClient(message.Client) + } + default: + log.Printf("unknown message type: %s", message.Message.Type) + h.closeClient(message.Client) + } + case <-ticker.C: + log.Printf("state: %d", h.state) + if h.state == 4 { + if time.Now().After(h.finishTime) { + h.state = 5 + clientAndScores := make(map[*GameClient]*int) + for client := range h.clients { + clientAndScores[client] = client.score + } + for client, score := range clientAndScores { + var opponentScore *int + for c2, s2 := range clientAndScores { + if c2 != client { + opponentScore = s2 + break + } + } + client.send <- &Message{Type: "finish", Data: MessageDataFinish{YourScore: score, OpponentScore: opponentScore}} + } + } + } + } + } +} + +func (h *GameHub) Close() { + for client := range h.clients { + h.closeClient(client) + } + close(h.receive) + close(h.register) + close(h.unregister) + for watcher := range h.watchers { + h.closeWatcher(watcher) + } + close(h.registerWatcher) + close(h.unregisterWatcher) +} + +func (h *GameHub) closeClient(client *GameClient) { + delete(h.clients, client) + close(client.send) +} + +func (h *GameHub) closeWatcher(watcher *GameWatcher) { + delete(h.watchers, watcher) + close(watcher.send) +} + +type GameClient struct { + hub *GameHub + conn *websocket.Conn + send chan *Message + score *int + code string + team string +} + +type GameWatcher struct { + hub *GameHub + conn *websocket.Conn + send chan *Message +} + +// Receives messages from the client and sends them to the hub. +func (c *GameClient) readPump() { + defer func() { + log.Printf("closing client") + c.hub.unregister <- c + c.conn.Close() + }() + c.conn.SetReadLimit(maxMessageSize) + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + var message Message + err := c.conn.ReadJSON(&message) + if err != nil { + log.Printf("error: %v", err) + return + } + c.hub.receive <- &MessageWithClient{c, &message} + } +} + +// Receives messages from the hub and sends them to the client. +func (c *GameClient) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + }() + for { + select { + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if !ok { + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + err := c.conn.WriteJSON(message) + if err != nil { + return + } + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +// Receives messages from the client and sends them to the hub. +func (c *GameWatcher) readPump() { + c.conn.SetReadLimit(maxMessageSize) + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) +} + +// Receives messages from the hub and sends them to the client. +func (c *GameWatcher) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + log.Printf("closing watcher") + c.hub.unregisterWatcher <- c + }() + for { + select { + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if !ok { + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + err := c.conn.WriteJSON(message) + if err != nil { + return + } + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +type GameHubs struct { + hubs map[int]*GameHub +} + +func NewGameHubs() *GameHubs { + return &GameHubs{ + hubs: make(map[int]*GameHub), + } +} + +func (hubs *GameHubs) Close() { + for _, hub := range hubs.hubs { + hub.Close() + } +} + +func (hubs *GameHubs) RestoreFromDB(ctx context.Context, q *db.Queries) error { + games, err := q.ListGames(ctx) + if err != nil { + return err + } + _ = games + return nil +} + +func (hubs *GameHubs) SockHandler() *sockHandler { + return newSockHandler(hubs) +} diff --git a/backend/game/http.go b/backend/game/http.go new file mode 100644 index 0000000..a5a7ded --- /dev/null +++ b/backend/game/http.go @@ -0,0 +1,56 @@ +package game + +import ( + "net/http" + "strconv" + + "github.com/labstack/echo/v4" +) + +type sockHandler struct { + hubs *GameHubs +} + +func newSockHandler(hubs *GameHubs) *sockHandler { + return &sockHandler{ + hubs: hubs, + } +} + +func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error { + gameId := c.Param("gameId") + gameIdInt, err := strconv.Atoi(gameId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") + } + var foundHub *GameHub + for _, hub := range h.hubs.hubs { + if hub.game.gameID == gameIdInt { + foundHub = hub + break + } + } + if foundHub == nil { + return echo.NewHTTPError(http.StatusNotFound, "Game not found") + } + return servePlayerWs(foundHub, c.Response(), c.Request(), "a") +} + +func (h *sockHandler) HandleSockGolfWatch(c echo.Context) error { + gameId := c.Param("gameId") + gameIdInt, err := strconv.Atoi(gameId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") + } + var foundHub *GameHub + for _, hub := range h.hubs.hubs { + if hub.game.gameID == gameIdInt { + foundHub = hub + break + } + } + if foundHub == nil { + return echo.NewHTTPError(http.StatusNotFound, "Game not found") + } + return serveWatcherWs(foundHub, c.Response(), c.Request()) +} diff --git a/backend/game/message.go b/backend/game/message.go new file mode 100644 index 0000000..7d1a166 --- /dev/null +++ b/backend/game/message.go @@ -0,0 +1,104 @@ +package game + +import ( + "encoding/json" + "fmt" +) + +type MessageWithClient struct { + Client *GameClient + Message *Message +} + +type Message struct { + Type string `json:"type"` + Data MessageData `json:"data"` +} + +type MessageData interface{} + +type MessageDataConnect struct { +} + +type MessageDataPrepare struct { + Problem string `json:"problem"` +} + +type MessageDataReady struct { +} + +type MessageDataStart struct { + StartTime string `json:"startTime"` +} + +type MessageDataCode struct { + Code string `json:"code"` +} + +type MessageDataScore struct { + Score int `json:"score"` +} + +type MessageDataFinish struct { + YourScore *int `json:"yourScore"` + OpponentScore *int `json:"opponentScore"` +} + +type MessageDataWatch struct { + Problem string `json:"problem"` + ScoreA *int `json:"scoreA"` + CodeA string `json:"codeA"` + ScoreB *int `json:"scoreB"` + CodeB string `json:"codeB"` +} + +func (m *Message) UnmarshalJSON(data []byte) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if err := json.Unmarshal(raw["type"], &m.Type); err != nil { + return err + } + + var err error + switch m.Type { + case "connect": + var data MessageDataConnect + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "prepare": + var data MessageDataPrepare + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "ready": + var data MessageDataReady + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "start": + var data MessageDataStart + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "code": + var data MessageDataCode + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "score": + var data MessageDataScore + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "finish": + var data MessageDataFinish + err = json.Unmarshal(raw["data"], &data) + m.Data = data + case "watch": + var data MessageDataWatch + err = json.Unmarshal(raw["data"], &data) + m.Data = data + default: + err = fmt.Errorf("unknown message type: %s", m.Type) + } + + return err +} diff --git a/backend/game/ws.go b/backend/game/ws.go new file mode 100644 index 0000000..2ed17af --- /dev/null +++ b/backend/game/ws.go @@ -0,0 +1,46 @@ +package game + +import ( + "net/http" + "time" + + "github.com/gorilla/websocket" +) + +const ( + writeWait = 10 * time.Second + pongWait = 60 * time.Second + pingPeriod = (pongWait * 9) / 10 + maxMessageSize = 512 +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func servePlayerWs(hub *GameHub, w http.ResponseWriter, r *http.Request, team string) error { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return err + } + client := &GameClient{hub: hub, conn: conn, send: make(chan *Message), team: team} + client.hub.register <- client + + go client.writePump() + go client.readPump() + return nil +} + +func serveWatcherWs(hub *GameHub, w http.ResponseWriter, r *http.Request) error { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return err + } + watcher := &GameWatcher{hub: hub, conn: conn, send: make(chan *Message)} + watcher.hub.registerWatcher <- watcher + + go watcher.writePump() + go watcher.readPump() + return nil +} diff --git a/backend/main.go b/backend/main.go index 46c7ed8..379fe8d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "net/http" - "strconv" "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" @@ -14,70 +13,9 @@ import ( "github.com/nsfisis/iosdc-2024-albatross/backend/api" "github.com/nsfisis/iosdc-2024-albatross/backend/db" + "github.com/nsfisis/iosdc-2024-albatross/backend/game" ) -const ( - gameTypeGolf = "golf" -) - -const ( - gameStateWaiting = "waiting" - gameStateReady = "ready" - gameStatePlaying = "playing" - gameStateFinished = "finished" -) - -type Game struct { - GameID int `db:"game_id"` - // "golf" - Type string `db:"type"` - CreatedAt string `db:"created_at"` - State string `db:"state"` -} - -var gameHubs = map[int]*GameHub{} - -func startGame(game *Game) { - if gameHubs[game.GameID] != nil { - return - } - gameHubs[game.GameID] = NewGameHub(game) - go gameHubs[game.GameID].Run() -} - -/* -func handleGolfPost(w http.ResponseWriter, r *http.Request) { - var yourTeam string - waitingGolfGames := []Game{} - err := db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting) - if err != nil { - http.Error(w, "Error getting games", http.StatusInternalServerError) - return - } - if len(waitingGolfGames) == 0 { - _, err = db.Exec("INSERT INTO games (type, state) VALUES ($1, $2)", gameTypeGolf, gameStateWaiting) - if err != nil { - http.Error(w, "Error creating game", http.StatusInternalServerError) - return - } - waitingGolfGames = []Game{} - err = db.Select(&waitingGolfGames, "SELECT * FROM games WHERE type = $1 AND state = $2 ORDER BY created_at", gameTypeGolf, gameStateWaiting) - if err != nil { - http.Error(w, "Error getting games", http.StatusInternalServerError) - return - } - yourTeam = "a" - startGame(&waitingGolfGames[0]) - } else { - yourTeam = "b" - db.Exec("UPDATE games SET state = $1 WHERE game_id = $2", gameStateReady, waitingGolfGames[0].GameID) - } - waitingGame := waitingGolfGames[0] - - http.Redirect(w, r, fmt.Sprintf("/golf/%d/%s/", waitingGame.GameID, yourTeam), http.StatusSeeOther) -} -*/ - func main() { var err error config, err := NewConfigFromEnv() @@ -105,59 +43,28 @@ func main() { e.Use(middleware.Logger()) e.Use(middleware.Recover()) - { - apiGroup := e.Group("/api") - apiGroup.Use(oapimiddleware.OapiRequestValidator(openApiSpec)) - apiHandler := api.NewHandler(queries) - api.RegisterHandlers(apiGroup, api.NewStrictHandler(apiHandler, []api.StrictMiddlewareFunc{ - api.NewJWTMiddleware(), - })) - } + apiGroup := e.Group("/api") + apiGroup.Use(oapimiddleware.OapiRequestValidator(openApiSpec)) + apiHandler := api.NewHandler(queries) + api.RegisterHandlers(apiGroup, api.NewStrictHandler(apiHandler, []api.StrictMiddlewareFunc{ + api.NewJWTMiddleware(), + })) - e.GET("/sock/golf/:gameId/watch", func(c echo.Context) error { - gameId := c.Param("gameId") - gameIdInt, err := strconv.Atoi(gameId) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") - } - var hub *GameHub - for _, h := range gameHubs { - if h.game.GameID == gameIdInt { - hub = h - break - } - } - if hub == nil { - return echo.NewHTTPError(http.StatusNotFound, "Game not found") - } - return serveWsWatcher(hub, c.Response(), c.Request()) + gameHubs := game.NewGameHubs() + err = gameHubs.RestoreFromDB(ctx, queries) + if err != nil { + log.Fatalf("Error restoring game hubs from db %v", err) + } + defer gameHubs.Close() + sockGroup := e.Group("/sock") + sockHandler := gameHubs.SockHandler() + sockGroup.GET("/golf/:gameId/watch", func(c echo.Context) error { + return sockHandler.HandleSockGolfWatch(c) }) - - e.GET("/sock/golf/:gameId/play", func(c echo.Context) error { - gameId := c.Param("gameId") - gameIdInt, err := strconv.Atoi(gameId) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") - } - var hub *GameHub - for _, h := range gameHubs { - if h.game.GameID == gameIdInt { - hub = h - break - } - } - if hub == nil { - return echo.NewHTTPError(http.StatusNotFound, "Game not found") - } - return serveWs(hub, c.Response(), c.Request(), "a") + sockGroup.GET("/golf/:gameId/play", func(c echo.Context) error { + return sockHandler.HandleSockGolfPlay(c) }) - defer func() { - for _, hub := range gameHubs { - hub.Close() - } - }() - if err := e.Start(":80"); err != http.ErrServerClosed { log.Fatal(err) } diff --git a/backend/message.go b/backend/message.go deleted file mode 100644 index f466a8f..0000000 --- a/backend/message.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" -) - -type MessageWithClient struct { - Client *GameClient - Message *Message -} - -type Message struct { - Type string `json:"type"` - Data MessageData `json:"data"` -} - -type MessageData interface{} - -type MessageDataConnect struct { -} - -type MessageDataPrepare struct { - Problem string `json:"problem"` -} - -type MessageDataReady struct { -} - -type MessageDataStart struct { - StartTime string `json:"startTime"` -} - -type MessageDataCode struct { - Code string `json:"code"` -} - -type MessageDataScore struct { - Score int `json:"score"` -} - -type MessageDataFinish struct { - YourScore *int `json:"yourScore"` - OpponentScore *int `json:"opponentScore"` -} - -type MessageDataWatch struct { - Problem string `json:"problem"` - ScoreA *int `json:"scoreA"` - CodeA string `json:"codeA"` - ScoreB *int `json:"scoreB"` - CodeB string `json:"codeB"` -} - -func (m *Message) UnmarshalJSON(data []byte) error { - var raw map[string]json.RawMessage - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if err := json.Unmarshal(raw["type"], &m.Type); err != nil { - return err - } - - var err error - switch m.Type { - case "connect": - var data MessageDataConnect - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "prepare": - var data MessageDataPrepare - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "ready": - var data MessageDataReady - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "start": - var data MessageDataStart - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "code": - var data MessageDataCode - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "score": - var data MessageDataScore - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "finish": - var data MessageDataFinish - err = json.Unmarshal(raw["data"], &data) - m.Data = data - case "watch": - var data MessageDataWatch - err = json.Unmarshal(raw["data"], &data) - m.Data = data - default: - err = fmt.Errorf("unknown message type: %s", m.Type) - } - - return err -} -- cgit v1.2.3-70-g09d2