diff options
Diffstat (limited to 'backend/game')
| -rw-r--r-- | backend/game/client.go | 113 | ||||
| -rw-r--r-- | backend/game/game.go | 385 | ||||
| -rw-r--r-- | backend/game/http.go | 6 | ||||
| -rw-r--r-- | backend/game/hub.go | 273 | ||||
| -rw-r--r-- | backend/game/message.go | 143 | ||||
| -rw-r--r-- | backend/game/models.go | 34 | ||||
| -rw-r--r-- | backend/game/ws.go | 28 |
7 files changed, 498 insertions, 484 deletions
diff --git a/backend/game/client.go b/backend/game/client.go new file mode 100644 index 0000000..7cd66a4 --- /dev/null +++ b/backend/game/client.go @@ -0,0 +1,113 @@ +package game + +import ( + "encoding/json" + "log" + "time" + + "github.com/gorilla/websocket" +) + +type playerClient struct { + hub *gameHub + conn *websocket.Conn + s2cMessages chan playerMessageS2C +} + +// Receives messages from the client and sends them to the hub. +func (c *playerClient) readPump() { + defer func() { + log.Printf("closing client") + c.hub.unregisterPlayer <- 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 rawMessage map[string]json.RawMessage + if err := c.conn.ReadJSON(&rawMessage); err != nil { + log.Printf("error: %v", err) + return + } + message, err := asPlayerMessageC2S(rawMessage) + if err != nil { + log.Printf("error: %v", err) + return + } + c.hub.playerC2SMessages <- &playerMessageC2SWithClient{c, message} + } +} + +// Receives messages from the hub and sends them to the client. +func (c *playerClient) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + }() + for { + select { + case message, ok := <-c.s2cMessages: + 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 watcherClient struct { + hub *gameHub + conn *websocket.Conn + s2cMessages chan watcherMessageS2C +} + +// Receives messages from the client and sends them to the hub. +func (c *watcherClient) 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 *watcherClient) 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.s2cMessages: + 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 deleted file mode 100644 index 9e63a1e..0000000 --- a/backend/game/game.go +++ /dev/null @@ -1,385 +0,0 @@ -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 index a5a7ded..beda46c 100644 --- a/backend/game/http.go +++ b/backend/game/http.go @@ -18,12 +18,13 @@ func newSockHandler(hubs *GameHubs) *sockHandler { } func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error { + // TODO: auth gameId := c.Param("gameId") gameIdInt, err := strconv.Atoi(gameId) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") } - var foundHub *GameHub + var foundHub *gameHub for _, hub := range h.hubs.hubs { if hub.game.gameID == gameIdInt { foundHub = hub @@ -37,12 +38,13 @@ func (h *sockHandler) HandleSockGolfPlay(c echo.Context) error { } func (h *sockHandler) HandleSockGolfWatch(c echo.Context) error { + // TODO: auth gameId := c.Param("gameId") gameIdInt, err := strconv.Atoi(gameId) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") } - var foundHub *GameHub + var foundHub *gameHub for _, hub := range h.hubs.hubs { if hub.game.gameID == gameIdInt { foundHub = hub diff --git a/backend/game/hub.go b/backend/game/hub.go new file mode 100644 index 0000000..c61f2bb --- /dev/null +++ b/backend/game/hub.go @@ -0,0 +1,273 @@ +package game + +import ( + "context" + "log" + "time" + + "github.com/jackc/pgx/v5/pgtype" + + "github.com/nsfisis/iosdc-2024-albatross/backend/api" + "github.com/nsfisis/iosdc-2024-albatross/backend/db" +) + +type playerClientState int + +const ( + playerClientStateWaitingEntries playerClientState = iota + playerClientStateEntried + playerClientStateReady +) + +type gameHub struct { + ctx context.Context + game *game + q *db.Queries + players map[*playerClient]playerClientState + registerPlayer chan *playerClient + unregisterPlayer chan *playerClient + playerC2SMessages chan *playerMessageC2SWithClient + watchers map[*watcherClient]bool + registerWatcher chan *watcherClient + unregisterWatcher chan *watcherClient +} + +func newGameHub(ctx context.Context, game *game, q *db.Queries) *gameHub { + return &gameHub{ + ctx: ctx, + game: game, + q: q, + players: make(map[*playerClient]playerClientState), + registerPlayer: make(chan *playerClient), + unregisterPlayer: make(chan *playerClient), + playerC2SMessages: make(chan *playerMessageC2SWithClient), + watchers: make(map[*watcherClient]bool), + registerWatcher: make(chan *watcherClient), + unregisterWatcher: make(chan *watcherClient), + } +} + +func (hub *gameHub) run() { + ticker := time.NewTicker(10 * time.Second) + defer func() { + ticker.Stop() + }() + + for { + select { + case player := <-hub.registerPlayer: + hub.players[player] = playerClientStateWaitingEntries + case player := <-hub.unregisterPlayer: + if _, ok := hub.players[player]; ok { + hub.closePlayerClient(player) + } + case watcher := <-hub.registerWatcher: + hub.watchers[watcher] = true + case watcher := <-hub.unregisterWatcher: + if _, ok := hub.watchers[watcher]; ok { + hub.closeWatcherClient(watcher) + } + case message := <-hub.playerC2SMessages: + switch msg := message.message.(type) { + case *playerMessageC2SEntry: + log.Printf("entry: %v", message.message) + // TODO: assert state is waiting_entries + hub.players[message.client] = playerClientStateEntried + entriedPlayerCount := 0 + for _, state := range hub.players { + if playerClientStateEntried <= state { + entriedPlayerCount++ + } + } + if entriedPlayerCount == 2 { + for player := range hub.players { + player.s2cMessages <- &playerMessageS2CPrepare{ + Type: playerMessageTypeS2CPrepare, + Data: playerMessageS2CPreparePayload{ + Problem: api.Problem{ + ProblemId: 1, + Title: "the answer", + Description: "print 42", + }, + }, + } + } + err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{ + GameID: int32(hub.game.gameID), + State: string(gameStatePrepare), + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.state = gameStatePrepare + } + case *playerMessageC2SReady: + log.Printf("ready: %v", message.message) + // TODO: assert state is prepare + hub.players[message.client] = playerClientStateReady + readyPlayerCount := 0 + for _, state := range hub.players { + if playerClientStateReady <= state { + readyPlayerCount++ + } + } + if readyPlayerCount == 2 { + startAt := time.Now().Add(11 * time.Second).UTC() + for player := range hub.players { + player.s2cMessages <- &playerMessageS2CStart{ + Type: playerMessageTypeS2CStart, + Data: playerMessageS2CStartPayload{ + StartAt: int(startAt.Unix()), + }, + } + } + err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{ + GameID: int32(hub.game.gameID), + StartedAt: pgtype.Timestamp{ + Time: startAt, + InfinityModifier: pgtype.Finite, + Valid: true, + }, + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.startedAt = &startAt + err = hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{ + GameID: int32(hub.game.gameID), + State: string(gameStateStarting), + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.state = gameStateStarting + } + case *playerMessageC2SCode: + // TODO: assert game state is gaming + log.Printf("code: %v", message.message) + code := msg.Data.Code + score := len(code) + message.client.s2cMessages <- &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Score: &score, + Status: api.Success, + }, + } + default: + log.Fatalf("unexpected message type: %T", message.message) + } + case <-ticker.C: + if hub.game.state == gameStateStarting { + if time.Now().After(*hub.game.startedAt) { + err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{ + GameID: int32(hub.game.gameID), + State: string(gameStateGaming), + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.state = gameStateGaming + } + } else if hub.game.state == gameStateGaming { + if time.Now().After(hub.game.startedAt.Add(time.Duration(hub.game.durationSeconds) * time.Second)) { + err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{ + GameID: int32(hub.game.gameID), + State: string(gameStateFinished), + }) + if err != nil { + log.Fatalf("failed to set game state: %v", err) + } + hub.game.state = gameStateFinished + } + hub.close() + return + } + } + } +} + +func (hub *gameHub) close() { + for client := range hub.players { + hub.closePlayerClient(client) + } + close(hub.registerPlayer) + close(hub.unregisterPlayer) + close(hub.playerC2SMessages) + for watcher := range hub.watchers { + hub.closeWatcherClient(watcher) + } + close(hub.registerWatcher) + close(hub.unregisterWatcher) +} + +func (hub *gameHub) closePlayerClient(player *playerClient) { + delete(hub.players, player) + close(player.s2cMessages) +} + +func (hub *gameHub) closeWatcherClient(watcher *watcherClient) { + delete(hub.watchers, watcher) + close(watcher.s2cMessages) +} + +type GameHubs struct { + hubs map[int]*gameHub + q *db.Queries +} + +func NewGameHubs(q *db.Queries) *GameHubs { + return &GameHubs{ + hubs: make(map[int]*gameHub), + q: q, + } +} + +func (hubs *GameHubs) Close() { + for _, hub := range hubs.hubs { + hub.close() + } +} + +func (hubs *GameHubs) RestoreFromDB(ctx context.Context) error { + games, err := hubs.q.ListGames(ctx) + if err != nil { + return err + } + for _, row := range games { + var startedAt *time.Time + if row.StartedAt.Valid { + startedAt = &row.StartedAt.Time + } + var problem_ *problem + if row.ProblemID.Valid { + if !row.Title.Valid || !row.Description.Valid { + panic("inconsistent data") + } + problem_ = &problem{ + problemID: int(row.ProblemID.Int32), + title: row.Title.String, + description: row.Description.String, + } + } + hubs.hubs[int(row.GameID)] = newGameHub(ctx, &game{ + gameID: int(row.GameID), + durationSeconds: int(row.DurationSeconds), + state: gameState(row.State), + displayName: row.DisplayName, + startedAt: startedAt, + problem: problem_, + }, hubs.q) + } + return nil +} + +func (hubs *GameHubs) Run() { + for _, hub := range hubs.hubs { + go hub.run() + } +} + +func (hubs *GameHubs) SockHandler() *sockHandler { + return newSockHandler(hubs) +} diff --git a/backend/game/message.go b/backend/game/message.go index 7d1a166..9116bde 100644 --- a/backend/game/message.go +++ b/backend/game/message.go @@ -3,102 +3,67 @@ 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"` -} + "github.com/nsfisis/iosdc-2024-albatross/backend/api" +) -type MessageDataFinish struct { - YourScore *int `json:"yourScore"` - OpponentScore *int `json:"opponentScore"` -} +const ( + playerMessageTypeS2CPrepare = "player:s2c:prepare" + playerMessageTypeS2CStart = "player:s2c:start" + playerMessageTypeS2CExecResult = "player:s2c:execreslut" + playerMessageTypeC2SEntry = "player:c2s:entry" + playerMessageTypeC2SReady = "player:c2s:ready" + playerMessageTypeC2SCode = "player:c2s:code" +) -type MessageDataWatch struct { - Problem string `json:"problem"` - ScoreA *int `json:"scoreA"` - CodeA string `json:"codeA"` - ScoreB *int `json:"scoreB"` - CodeB string `json:"codeB"` +type playerMessageC2SWithClient struct { + client *playerClient + message playerMessageC2S } -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 +type playerMessage = api.GamePlayerMessage + +type playerMessageS2C = interface{} +type playerMessageS2CPrepare = api.GamePlayerMessageS2CPrepare +type playerMessageS2CPreparePayload = api.GamePlayerMessageS2CPreparePayload +type playerMessageS2CStart = api.GamePlayerMessageS2CStart +type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload +type playerMessageS2CExecResult = api.GamePlayerMessageS2CExecResult +type playerMessageS2CExecResultPayload = api.GamePlayerMessageS2CExecResultPayload + +type playerMessageC2S = interface{} +type playerMessageC2SEntry = api.GamePlayerMessageC2SEntry +type playerMessageC2SReady = api.GamePlayerMessageC2SReady +type playerMessageC2SCode = api.GamePlayerMessageC2SCode +type playerMessageC2SCodePayload = api.GamePlayerMessageC2SCodePayload + +func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error) { + var typ string + if err := json.Unmarshal(raw["type"], &typ); err != nil { + return nil, 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 + switch typ { + case playerMessageTypeC2SEntry: + return &playerMessageC2SEntry{ + Type: playerMessageTypeC2SEntry, + }, nil + case playerMessageTypeC2SReady: + return &playerMessageC2SReady{ + Type: playerMessageTypeC2SReady, + }, nil + case playerMessageTypeC2SCode: + var payload playerMessageC2SCodePayload + if err := json.Unmarshal(raw["data"], &payload); err != nil { + return nil, err + } + return &playerMessageC2SCode{ + Type: playerMessageTypeC2SCode, + Data: payload, + }, nil default: - err = fmt.Errorf("unknown message type: %s", m.Type) + return nil, fmt.Errorf("unknown message type: %s", typ) } - - return err } + +type watcherMessageS2C = interface{} diff --git a/backend/game/models.go b/backend/game/models.go new file mode 100644 index 0000000..4e9ee87 --- /dev/null +++ b/backend/game/models.go @@ -0,0 +1,34 @@ +package game + +import ( + "time" + + "github.com/nsfisis/iosdc-2024-albatross/backend/api" +) + +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 gameState + displayName string + durationSeconds int + startedAt *time.Time + problem *problem +} + +type problem struct { + problemID int + title string + description string +} diff --git a/backend/game/ws.go b/backend/game/ws.go index 2ed17af..013db7a 100644 --- a/backend/game/ws.go +++ b/backend/game/ws.go @@ -17,28 +17,40 @@ const ( var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // TODO: insecure! + return true + }, } -func servePlayerWs(hub *GameHub, w http.ResponseWriter, r *http.Request, team string) error { +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 + player := &playerClient{ + hub: hub, + conn: conn, + s2cMessages: make(chan playerMessageS2C), + } + hub.registerPlayer <- player - go client.writePump() - go client.readPump() + go player.writePump() + go player.readPump() return nil } -func serveWatcherWs(hub *GameHub, w http.ResponseWriter, r *http.Request) error { +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 + watcher := &watcherClient{ + hub: hub, + conn: conn, + s2cMessages: make(chan watcherMessageS2C), + } + hub.registerWatcher <- watcher go watcher.writePump() go watcher.readPump() |
