aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/game
diff options
context:
space:
mode:
Diffstat (limited to 'backend/game')
-rw-r--r--backend/game/client.go113
-rw-r--r--backend/game/game.go385
-rw-r--r--backend/game/http.go6
-rw-r--r--backend/game/hub.go273
-rw-r--r--backend/game/message.go143
-rw-r--r--backend/game/models.go34
-rw-r--r--backend/game/ws.go28
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()