aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-07-28 20:09:09 +0900
committernsfisis <nsfisis@gmail.com>2024-07-29 02:32:26 +0900
commitdaaf81ae931654e20f882fbc6bbc4a02cbfc0273 (patch)
tree2d0eb7c9f40e28a4295dc1065c80d5d891a8995a /backend
parent0b90018afbd438d61db7b41e5c3ea41cbb563bfe (diff)
downloadiosdc-japan-2024-albatross-daaf81ae931654e20f882fbc6bbc4a02cbfc0273.tar.gz
iosdc-japan-2024-albatross-daaf81ae931654e20f882fbc6bbc4a02cbfc0273.tar.zst
iosdc-japan-2024-albatross-daaf81ae931654e20f882fbc6bbc4a02cbfc0273.zip
feat(backend): partially implement gaming
Diffstat (limited to 'backend')
-rw-r--r--backend/api/generated.go140
-rw-r--r--backend/api/handlers.go35
-rw-r--r--backend/db/query.sql.go70
-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.go250
-rw-r--r--backend/game/message.go122
-rw-r--r--backend/game/models.go34
-rw-r--r--backend/game/ws.go28
-rw-r--r--backend/main.go12
-rw-r--r--backend/query.sql16
12 files changed, 708 insertions, 503 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go
index 922fb55..86e7567 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -114,6 +114,11 @@ type GetGamesParams struct {
Authorization string `json:"Authorization"`
}
+// GetGamesGameIdParams defines parameters for GetGamesGameId.
+type GetGamesGameIdParams struct {
+ Authorization string `json:"Authorization"`
+}
+
// PostLoginJSONBody defines parameters for PostLogin.
type PostLoginJSONBody struct {
Password string `json:"password"`
@@ -314,6 +319,9 @@ type ServerInterface interface {
// List games
// (GET /games)
GetGames(ctx echo.Context, params GetGamesParams) error
+ // Get a game
+ // (GET /games/{game_id})
+ GetGamesGameId(ctx echo.Context, gameId int, params GetGamesGameIdParams) error
// User login
// (POST /login)
PostLogin(ctx echo.Context) error
@@ -361,6 +369,44 @@ func (w *ServerInterfaceWrapper) GetGames(ctx echo.Context) error {
return err
}
+// GetGamesGameId converts echo context to params.
+func (w *ServerInterfaceWrapper) GetGamesGameId(ctx echo.Context) error {
+ var err error
+ // ------------- Path parameter "game_id" -------------
+ var gameId int
+
+ err = runtime.BindStyledParameterWithOptions("simple", "game_id", ctx.Param("game_id"), &gameId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter game_id: %s", err))
+ }
+
+ // Parameter object where we will unmarshal all parameters from the context
+ var params GetGamesGameIdParams
+
+ headers := ctx.Request().Header
+ // ------------- Required header parameter "Authorization" -------------
+ if valueList, found := headers[http.CanonicalHeaderKey("Authorization")]; found {
+ var Authorization string
+ n := len(valueList)
+ if n != 1 {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for Authorization, got %d", n))
+ }
+
+ err = runtime.BindStyledParameterWithOptions("simple", "Authorization", valueList[0], &Authorization, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: true})
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter Authorization: %s", err))
+ }
+
+ params.Authorization = Authorization
+ } else {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter Authorization is required, but not found"))
+ }
+
+ // Invoke the callback with all the unmarshaled arguments
+ err = w.Handler.GetGamesGameId(ctx, gameId, params)
+ return err
+}
+
// PostLogin converts echo context to params.
func (w *ServerInterfaceWrapper) PostLogin(ctx echo.Context) error {
var err error
@@ -399,6 +445,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
}
router.GET(baseURL+"/games", wrapper.GetGames)
+ router.GET(baseURL+"/games/:game_id", wrapper.GetGamesGameId)
router.POST(baseURL+"/login", wrapper.PostLogin)
}
@@ -433,6 +480,35 @@ func (response GetGames403JSONResponse) VisitGetGamesResponse(w http.ResponseWri
return json.NewEncoder(w).Encode(response)
}
+type GetGamesGameIdRequestObject struct {
+ GameId int `json:"game_id"`
+ Params GetGamesGameIdParams
+}
+
+type GetGamesGameIdResponseObject interface {
+ VisitGetGamesGameIdResponse(w http.ResponseWriter) error
+}
+
+type GetGamesGameId200JSONResponse Game
+
+func (response GetGamesGameId200JSONResponse) VisitGetGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
+type GetGamesGameId403JSONResponse struct {
+ Message string `json:"message"`
+}
+
+func (response GetGamesGameId403JSONResponse) VisitGetGamesGameIdResponse(w http.ResponseWriter) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(403)
+
+ return json.NewEncoder(w).Encode(response)
+}
+
type PostLoginRequestObject struct {
Body *PostLoginJSONRequestBody
}
@@ -468,6 +544,9 @@ type StrictServerInterface interface {
// List games
// (GET /games)
GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error)
+ // Get a game
+ // (GET /games/{game_id})
+ GetGamesGameId(ctx context.Context, request GetGamesGameIdRequestObject) (GetGamesGameIdResponseObject, error)
// User login
// (POST /login)
PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error)
@@ -510,6 +589,32 @@ func (sh *strictHandler) GetGames(ctx echo.Context, params GetGamesParams) error
return nil
}
+// GetGamesGameId operation middleware
+func (sh *strictHandler) GetGamesGameId(ctx echo.Context, gameId int, params GetGamesGameIdParams) error {
+ var request GetGamesGameIdRequestObject
+
+ request.GameId = gameId
+ request.Params = params
+
+ handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
+ return sh.ssi.GetGamesGameId(ctx.Request().Context(), request.(GetGamesGameIdRequestObject))
+ }
+ for _, middleware := range sh.middlewares {
+ handler = middleware(handler, "GetGamesGameId")
+ }
+
+ response, err := handler(ctx, request)
+
+ if err != nil {
+ return err
+ } else if validResponse, ok := response.(GetGamesGameIdResponseObject); ok {
+ return validResponse.VisitGetGamesGameIdResponse(ctx.Response())
+ } else if response != nil {
+ return fmt.Errorf("unexpected response type: %T", response)
+ }
+ return nil
+}
+
// PostLogin operation middleware
func (sh *strictHandler) PostLogin(ctx echo.Context) error {
var request PostLoginRequestObject
@@ -542,23 +647,24 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/6xWW2+jRhT+K2jaR+RrFG15S9N2ldVWter2aRVZx3BsJoWZ2TlDsu6K/76aGQzG4Bgn",
- "zkNC4Fy+853rdxbLXEmBwhCLvjOKU8zBPX6EHO1fpaVCbTi6twknlcFuJaqv+A1ylSGLnHwwZSEzO2X/",
- "J6O52LIyZEmhwXApVoSxFAm19Oa3k1qFC4Nb1FZnCzmueNISnfYJKi3XGeZW8GeNGxaxn8ZNTOMqoPGi",
- "EitDRga0wWQFpmX9l5vb2w83Hya9cMiA8fGKImfRFxZnkjBhIXsBbrjYrlAYbTlq3jg/zCJEBRpZ5dmS",
- "4uLzDxsuOKWYsMfwgEyIDX/GLpllyDR+LbjGxKLYs7QHGLbz00P9Y21Srp8wNjY4m7lFBjvUfyIRbF2g",
- "UuBfGxZ9eZ3Wjupyds/K8EKl+9mSlY99SOyXt4O5ny1/F0bv3oTob4RkdxqWN9zpD0+ubStBhkVMObUo",
- "nlGETuNcSt3XQVmqMV4CQjuN64Gw+X5XtSyq7rg4RcvZ/dJ1WH+KDkx3hxgYODcwXjG4gF0mIbF8nGCa",
- "ZnHUtP0ArkOPaSjlR0A6AV46FI8g7dWHwvF5uBrNztwwkvcz9voUt0B0QnN+L9ggR4Bq9T40n15O+z29",
- "fj/JVAS/SexbwDyWYqXApG2VMc9hizR+kqkYPaltryqtIMm5aGluICOshddSZgjCSheEurO1Z/O+jWpF",
- "u1FYKGfTufdyYKSz+GrcfQwvmvY4ohcp1lzZjdnG9U/KKeAUQLDvjR6uqk+DzhbDTXYUe4Wq74bq70/P",
- "gbcUtrB3g7YmuNhItx68b3aXrcFoSRRYYFpAFrzgOrhbPLCQPaMmRwObjKajicUsFQpQnEVsPpqMJva0",
- "AZM64sb2EnFPW3RNYVl1l8dDYm9DNB+dgFXRkKNBTW5d2MpiXwt0m9HXQ9Xf1V3j5kSz1A5bqtJOERLU",
- "jfpdYVKp+f/OPTtkzugCe0zWLD9aYVJSkI9lNplUc8egcGGBUhmPneXxE/kqaey1i6mmhBvMacggbIYd",
- "A61h13vv0YnstmqXfeZkArkJvEYZspvJ/B2x5M1h2BTsH1KveZKgCOpsny3dvaEhMdT2nRUq8hzsyeVj",
- "qwIrQzbO5NYPKCWpp/gWksxnJ+KhIJlfpT+b3siGAqIXqdttXr+dzuZ90+GdA6+aa7XrfgLbtV5etZ6N",
- "/A+PxuI3+zM6+H1+FTsjQ7K/LOIYiTZFlu0CKEyKwliomPhynl67nB/EM2Q8CWKNifUFGV21nPf299kM",
- "pA7qdLYr/F9CHfiyLsuy/BEAAP//HvsQPaYPAAA=",
+ "H4sIAAAAAAAC/9xWUW/jNgz+K4a2RyNJ06K4+a3rtqKHGxYs29OhCBiLidXZkk6U28sK//dBkmPHsXNJ",
+ "2zwMdw851yapjx8/inxhqSq0kigtseSFUZphAf7xDgp0/2ujNBor0L/lgnQOm4Wsv+JXKHSOLPH20QWL",
+ "md1o9zdZI+SaVTHjpQErlFwQpkpy6vhdXk8aFyEtrtE4nzUUuBC8Y3oxZKiNWuZYOMMfDa5Ywn4YtzmN",
+ "64TGs9qsihlZMBb5Amwn+k9X19cfrj5MBuGQBRvylWXBks8szRUhZzF7BmGFXC9QWuM4at/4c5hDiBoM",
+ "svpkR4rPLzyshBSUIWcP8Q6ZkFrxhH0yq5gZ/FIKg9yh2LK0BRh36zNA/UMTUi0fMbUuOVe5WQ4bNL8j",
+ "Eax9okriHyuWfP42rT3X+fSWVfErnW6nc1Y9DCFxX94O5nY6/1Vas3kToj8R+OYwrBC41x+BXNdWkixL",
+ "mPZuSTqlBL3HsZL6rydVqcH4GhDGe5wPhKv3u9Qyq7vj1SWaT2/nvsOGS7QTun+JgYVjF8Y3As5gkyvg",
+ "jo8DTNM0Tdq2P4HrOGA6lfI9IL0EX3sp7kHaup8KJ9ThbDT7cKeRvL1jz09xB0QvNX/uKybIHqDGfQjN",
+ "x+fD5x4evx9VJqNfFA4NYJEqudBgs67LWBSwRho/qkyOHvV60JUWwAshO54ryAkb46VSOYJ01iWh6U3t",
+ "6eXQRHWm/SwclKPl3J6yE6Q3+BrcQwzP2vbYoxcpNUK7idnF9VcmKBIUQbTtjQGu6k8nrS1W2Hwv9xrV",
+ "0A413J+BgxAp7mDvJ+1CCLlSfjyEs9lNvgRrFFHkgBkJefSMy+hmds9i9oSGPA1sMroYTRxmpVGCFixh",
+ "l6PJaOJWG7CZJ27sNhH/tEbfFI5Vv3ncc7cbor3zBs7FQIEWDflx4ZTFvpToJ2PQQ93f9V7j74l2qO22",
+ "VO2dIXA0rftNaTNlxL/+eLbLnDUlDoRsWH5wxqSVpJDLdDKp7x2L0qcFWuci9ZHHjxRU0sbriqmhRFgs",
+ "6JSLsL3sGBgDm8F9jw5Ut6Nd9kmQjdQqCh5VzK4ml+/IpWgXw1awvymzFJyjjJpqH5XuNtApOTTxfRQq",
+ "iwLcyhVyqxOr4lp745d6Ga6OqtD93PMDWvTXZKOldsE+qqL/szCPC6/P/o2n+DuTzh3aCOrEnHRytQ6z",
+ "TSsaUMxMkf3kTQIUJPuzChv3G9nQQPSsTHdCNG8vppdDg+Wds7Ieic3RwwR21Vid9Sq06h/cm6hf3b/R",
+ "zu/xLc4HOaX68zJNkWhV5vkmgtJmKK2DijzI+eLccr6XT5ALHqUGuTsLcjqrnLfxt9WMlImacnYV/jeh",
+ "iYKsq6qq/gsAAP//bqjkD+ERAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/backend/api/handlers.go b/backend/api/handlers.go
index 54a3fc4..91cdbc8 100644
--- a/backend/api/handlers.go
+++ b/backend/api/handlers.go
@@ -133,6 +133,41 @@ func (h *ApiHandler) GetGames(ctx context.Context, request GetGamesRequestObject
}
}
+func (h *ApiHandler) GetGamesGameId(ctx context.Context, request GetGamesGameIdRequestObject) (GetGamesGameIdResponseObject, error) {
+ // TODO: user permission
+ // user := ctx.Value("user").(*auth.JWTClaims)
+ gameId := request.GameId
+ row, err := h.q.GetGameById(ctx, int32(gameId))
+ if err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ var startedAt *int
+ if row.StartedAt.Valid {
+ startedAtTimestamp := int(row.StartedAt.Time.Unix())
+ startedAt = &startedAtTimestamp
+ }
+ var problem *Problem
+ if row.ProblemID.Valid && GameState(row.State) != Closed && GameState(row.State) != WaitingEntries {
+ 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,
+ }
+ }
+ game := Game{
+ GameId: int(row.GameID),
+ State: GameState(row.State),
+ DisplayName: row.DisplayName,
+ DurationSeconds: int(row.DurationSeconds),
+ StartedAt: startedAt,
+ Problem: problem,
+ }
+ return GetGamesGameId200JSONResponse(game), nil
+}
+
func _assertJwtPayloadIsCompatibleWithJWTClaims() {
var c auth.JWTClaims
var p JwtPayload
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index 20a7dc1..1b9f392 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -11,6 +11,44 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
+const getGameById = `-- name: GetGameById :one
+SELECT game_id, state, display_name, duration_seconds, created_at, started_at, games.problem_id, problems.problem_id, title, description FROM games
+LEFT JOIN problems ON games.problem_id = problems.problem_id
+WHERE games.game_id = $1
+LIMIT 1
+`
+
+type GetGameByIdRow struct {
+ GameID int32
+ State string
+ DisplayName string
+ DurationSeconds int32
+ CreatedAt pgtype.Timestamp
+ StartedAt pgtype.Timestamp
+ ProblemID pgtype.Int4
+ ProblemID_2 pgtype.Int4
+ Title pgtype.Text
+ Description pgtype.Text
+}
+
+func (q *Queries) GetGameById(ctx context.Context, gameID int32) (GetGameByIdRow, error) {
+ row := q.db.QueryRow(ctx, getGameById, gameID)
+ var i GetGameByIdRow
+ err := row.Scan(
+ &i.GameID,
+ &i.State,
+ &i.DisplayName,
+ &i.DurationSeconds,
+ &i.CreatedAt,
+ &i.StartedAt,
+ &i.ProblemID,
+ &i.ProblemID_2,
+ &i.Title,
+ &i.Description,
+ )
+ return i, err
+}
+
const getUserAuthByUsername = `-- name: GetUserAuthByUsername :one
SELECT users.user_id, username, display_name, icon_path, is_admin, created_at, user_auth_id, user_auths.user_id, auth_type, password_hash FROM users
JOIN user_auths ON users.user_id = user_auths.user_id
@@ -172,3 +210,35 @@ func (q *Queries) ListGamesForPlayer(ctx context.Context, userID int32) ([]ListG
}
return items, nil
}
+
+const updateGameStartedAt = `-- name: UpdateGameStartedAt :exec
+UPDATE games
+SET started_at = $2
+WHERE game_id = $1
+`
+
+type UpdateGameStartedAtParams struct {
+ GameID int32
+ StartedAt pgtype.Timestamp
+}
+
+func (q *Queries) UpdateGameStartedAt(ctx context.Context, arg UpdateGameStartedAtParams) error {
+ _, err := q.db.Exec(ctx, updateGameStartedAt, arg.GameID, arg.StartedAt)
+ return err
+}
+
+const updateGameState = `-- name: UpdateGameState :exec
+UPDATE games
+SET state = $2
+WHERE game_id = $1
+`
+
+type UpdateGameStateParams struct {
+ GameID int32
+ State string
+}
+
+func (q *Queries) UpdateGameState(ctx context.Context, arg UpdateGameStateParams) error {
+ _, err := q.db.Exec(ctx, updateGameState, arg.GameID, arg.State)
+ return err
+}
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..170f142
--- /dev/null
+++ b/backend/game/hub.go
@@ -0,0 +1,250 @@
+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 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
+ }
+ default:
+ log.Fatalf("unexpected message type: %T", message.message)
+ }
+ case <-ticker.C:
+ 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..23774ce 100644
--- a/backend/game/message.go
+++ b/backend/game/message.go
@@ -3,102 +3,52 @@ 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"`
-}
+ "github.com/nsfisis/iosdc-2024-albatross/backend/api"
+)
-type MessageDataCode struct {
- Code string `json:"code"`
-}
+const (
+ playerMessageTypeS2CPrepare = "player:s2c:prepare"
+ playerMessageTypeS2CStart = "player:s2c:start"
+ playerMessageTypeC2SEntry = "player:c2s:entry"
+ playerMessageTypeC2SReady = "player:c2s:ready"
+)
-type MessageDataScore struct {
- Score int `json:"score"`
+type playerMessageC2SWithClient struct {
+ client *playerClient
+ message playerMessageC2S
}
-type MessageDataFinish struct {
- YourScore *int `json:"yourScore"`
- OpponentScore *int `json:"opponentScore"`
-}
+type playerMessage = api.GamePlayerMessage
-type MessageDataWatch struct {
- Problem string `json:"problem"`
- ScoreA *int `json:"scoreA"`
- CodeA string `json:"codeA"`
- ScoreB *int `json:"scoreB"`
- CodeB string `json:"codeB"`
-}
+type playerMessageS2C = interface{}
+type playerMessageS2CPrepare = api.GamePlayerMessageS2CPrepare
+type playerMessageS2CPreparePayload = api.GamePlayerMessageS2CPreparePayload
+type playerMessageS2CStart = api.GamePlayerMessageS2CStart
+type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload
-func (m *Message) UnmarshalJSON(data []byte) error {
- var raw map[string]json.RawMessage
- if err := json.Unmarshal(data, &raw); err != nil {
- return err
- }
+type playerMessageC2S = interface{}
+type playerMessageC2SEntry = api.GamePlayerMessageC2SEntry
+type playerMessageC2SReady = api.GamePlayerMessageC2SReady
- if err := json.Unmarshal(raw["type"], &m.Type); err != nil {
- return err
+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
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()
diff --git a/backend/main.go b/backend/main.go
index 8042674..91caa73 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -64,20 +64,22 @@ func main() {
api.NewJWTMiddleware(),
}))
- gameHubs := game.NewGameHubs()
- err = gameHubs.RestoreFromDB(ctx, queries)
+ gameHubs := game.NewGameHubs(queries)
+ err = gameHubs.RestoreFromDB(ctx)
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)
- })
sockGroup.GET("/golf/:gameId/play", func(c echo.Context) error {
return sockHandler.HandleSockGolfPlay(c)
})
+ sockGroup.GET("/golf/:gameId/watch", func(c echo.Context) error {
+ return sockHandler.HandleSockGolfWatch(c)
+ })
+
+ gameHubs.Run()
if err := e.Start(":80"); err != http.ErrServerClosed {
log.Fatal(err)
diff --git a/backend/query.sql b/backend/query.sql
index 9b038a5..b2ad2c4 100644
--- a/backend/query.sql
+++ b/backend/query.sql
@@ -18,3 +18,19 @@ SELECT * FROM games
LEFT JOIN problems ON games.problem_id = problems.problem_id
JOIN game_players ON games.game_id = game_players.game_id
WHERE game_players.user_id = $1;
+
+-- name: UpdateGameState :exec
+UPDATE games
+SET state = $2
+WHERE game_id = $1;
+
+-- name: UpdateGameStartedAt :exec
+UPDATE games
+SET started_at = $2
+WHERE game_id = $1;
+
+-- name: GetGameById :one
+SELECT * FROM games
+LEFT JOIN problems ON games.problem_id = problems.problem_id
+WHERE games.game_id = $1
+LIMIT 1;