aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/Dockerfile3
-rw-r--r--backend/api/generated.go455
-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.go273
-rw-r--r--backend/game/message.go143
-rw-r--r--backend/game/models.go34
-rw-r--r--backend/game/ws.go28
-rw-r--r--backend/main.go34
-rw-r--r--backend/query.sql16
13 files changed, 1088 insertions, 507 deletions
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 1d1523d..acf7a68 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,6 +1,9 @@
FROM golang:1.22.3 AS builder
WORKDIR /build
+COPY go.mod go.sum ./
+RUN go mod download
+
COPY . /build
RUN go build -o /build/server .
diff --git a/backend/api/generated.go b/backend/api/generated.go
index 921f4b3..e39e3ba 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -32,6 +32,11 @@ const (
WaitingStart GameState = "waiting_start"
)
+// Defines values for GamePlayerMessageS2CExecResultPayloadStatus.
+const (
+ Success GamePlayerMessageS2CExecResultPayloadStatus = "success"
+)
+
// Game defines model for Game.
type Game struct {
DisplayName string `json:"display_name"`
@@ -45,6 +50,79 @@ type Game struct {
// GameState defines model for Game.State.
type GameState string
+// GamePlayerMessage defines model for GamePlayerMessage.
+type GamePlayerMessage struct {
+ union json.RawMessage
+}
+
+// GamePlayerMessageC2S defines model for GamePlayerMessageC2S.
+type GamePlayerMessageC2S struct {
+ union json.RawMessage
+}
+
+// GamePlayerMessageC2SCode defines model for GamePlayerMessageC2SCode.
+type GamePlayerMessageC2SCode struct {
+ Data GamePlayerMessageC2SCodePayload `json:"data"`
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageC2SCodePayload defines model for GamePlayerMessageC2SCodePayload.
+type GamePlayerMessageC2SCodePayload struct {
+ Code string `json:"code"`
+}
+
+// GamePlayerMessageC2SEntry defines model for GamePlayerMessageC2SEntry.
+type GamePlayerMessageC2SEntry struct {
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageC2SReady defines model for GamePlayerMessageC2SReady.
+type GamePlayerMessageC2SReady struct {
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageS2C defines model for GamePlayerMessageS2C.
+type GamePlayerMessageS2C struct {
+ union json.RawMessage
+}
+
+// GamePlayerMessageS2CExecResult defines model for GamePlayerMessageS2CExecResult.
+type GamePlayerMessageS2CExecResult struct {
+ Data GamePlayerMessageS2CExecResultPayload `json:"data"`
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageS2CExecResultPayload defines model for GamePlayerMessageS2CExecResultPayload.
+type GamePlayerMessageS2CExecResultPayload struct {
+ Score *int `json:"score"`
+ Status GamePlayerMessageS2CExecResultPayloadStatus `json:"status"`
+}
+
+// GamePlayerMessageS2CExecResultPayloadStatus defines model for GamePlayerMessageS2CExecResultPayload.Status.
+type GamePlayerMessageS2CExecResultPayloadStatus string
+
+// GamePlayerMessageS2CPrepare defines model for GamePlayerMessageS2CPrepare.
+type GamePlayerMessageS2CPrepare struct {
+ Data GamePlayerMessageS2CPreparePayload `json:"data"`
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageS2CPreparePayload defines model for GamePlayerMessageS2CPreparePayload.
+type GamePlayerMessageS2CPreparePayload struct {
+ Problem Problem `json:"problem"`
+}
+
+// GamePlayerMessageS2CStart defines model for GamePlayerMessageS2CStart.
+type GamePlayerMessageS2CStart struct {
+ Data GamePlayerMessageS2CStartPayload `json:"data"`
+ Type string `json:"type"`
+}
+
+// GamePlayerMessageS2CStartPayload defines model for GamePlayerMessageS2CStartPayload.
+type GamePlayerMessageS2CStartPayload struct {
+ StartAt int `json:"start_at"`
+}
+
// JwtPayload defines model for JwtPayload.
type JwtPayload struct {
DisplayName string `json:"display_name"`
@@ -67,6 +145,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"`
@@ -76,11 +159,252 @@ type PostLoginJSONBody struct {
// PostLoginJSONRequestBody defines body for PostLogin for application/json ContentType.
type PostLoginJSONRequestBody PostLoginJSONBody
+// AsGamePlayerMessageS2C returns the union data inside the GamePlayerMessage as a GamePlayerMessageS2C
+func (t GamePlayerMessage) AsGamePlayerMessageS2C() (GamePlayerMessageS2C, error) {
+ var body GamePlayerMessageS2C
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageS2C overwrites any union data inside the GamePlayerMessage as the provided GamePlayerMessageS2C
+func (t *GamePlayerMessage) FromGamePlayerMessageS2C(v GamePlayerMessageS2C) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageS2C performs a merge with any union data inside the GamePlayerMessage, using the provided GamePlayerMessageS2C
+func (t *GamePlayerMessage) MergeGamePlayerMessageS2C(v GamePlayerMessageS2C) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+// AsGamePlayerMessageC2S returns the union data inside the GamePlayerMessage as a GamePlayerMessageC2S
+func (t GamePlayerMessage) AsGamePlayerMessageC2S() (GamePlayerMessageC2S, error) {
+ var body GamePlayerMessageC2S
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageC2S overwrites any union data inside the GamePlayerMessage as the provided GamePlayerMessageC2S
+func (t *GamePlayerMessage) FromGamePlayerMessageC2S(v GamePlayerMessageC2S) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageC2S performs a merge with any union data inside the GamePlayerMessage, using the provided GamePlayerMessageC2S
+func (t *GamePlayerMessage) MergeGamePlayerMessageC2S(v GamePlayerMessageC2S) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+func (t GamePlayerMessage) MarshalJSON() ([]byte, error) {
+ b, err := t.union.MarshalJSON()
+ return b, err
+}
+
+func (t *GamePlayerMessage) UnmarshalJSON(b []byte) error {
+ err := t.union.UnmarshalJSON(b)
+ return err
+}
+
+// AsGamePlayerMessageC2SEntry returns the union data inside the GamePlayerMessageC2S as a GamePlayerMessageC2SEntry
+func (t GamePlayerMessageC2S) AsGamePlayerMessageC2SEntry() (GamePlayerMessageC2SEntry, error) {
+ var body GamePlayerMessageC2SEntry
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageC2SEntry overwrites any union data inside the GamePlayerMessageC2S as the provided GamePlayerMessageC2SEntry
+func (t *GamePlayerMessageC2S) FromGamePlayerMessageC2SEntry(v GamePlayerMessageC2SEntry) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageC2SEntry performs a merge with any union data inside the GamePlayerMessageC2S, using the provided GamePlayerMessageC2SEntry
+func (t *GamePlayerMessageC2S) MergeGamePlayerMessageC2SEntry(v GamePlayerMessageC2SEntry) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+// AsGamePlayerMessageC2SReady returns the union data inside the GamePlayerMessageC2S as a GamePlayerMessageC2SReady
+func (t GamePlayerMessageC2S) AsGamePlayerMessageC2SReady() (GamePlayerMessageC2SReady, error) {
+ var body GamePlayerMessageC2SReady
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageC2SReady overwrites any union data inside the GamePlayerMessageC2S as the provided GamePlayerMessageC2SReady
+func (t *GamePlayerMessageC2S) FromGamePlayerMessageC2SReady(v GamePlayerMessageC2SReady) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageC2SReady performs a merge with any union data inside the GamePlayerMessageC2S, using the provided GamePlayerMessageC2SReady
+func (t *GamePlayerMessageC2S) MergeGamePlayerMessageC2SReady(v GamePlayerMessageC2SReady) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+// AsGamePlayerMessageC2SCode returns the union data inside the GamePlayerMessageC2S as a GamePlayerMessageC2SCode
+func (t GamePlayerMessageC2S) AsGamePlayerMessageC2SCode() (GamePlayerMessageC2SCode, error) {
+ var body GamePlayerMessageC2SCode
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageC2SCode overwrites any union data inside the GamePlayerMessageC2S as the provided GamePlayerMessageC2SCode
+func (t *GamePlayerMessageC2S) FromGamePlayerMessageC2SCode(v GamePlayerMessageC2SCode) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageC2SCode performs a merge with any union data inside the GamePlayerMessageC2S, using the provided GamePlayerMessageC2SCode
+func (t *GamePlayerMessageC2S) MergeGamePlayerMessageC2SCode(v GamePlayerMessageC2SCode) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+func (t GamePlayerMessageC2S) MarshalJSON() ([]byte, error) {
+ b, err := t.union.MarshalJSON()
+ return b, err
+}
+
+func (t *GamePlayerMessageC2S) UnmarshalJSON(b []byte) error {
+ err := t.union.UnmarshalJSON(b)
+ return err
+}
+
+// AsGamePlayerMessageS2CPrepare returns the union data inside the GamePlayerMessageS2C as a GamePlayerMessageS2CPrepare
+func (t GamePlayerMessageS2C) AsGamePlayerMessageS2CPrepare() (GamePlayerMessageS2CPrepare, error) {
+ var body GamePlayerMessageS2CPrepare
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageS2CPrepare overwrites any union data inside the GamePlayerMessageS2C as the provided GamePlayerMessageS2CPrepare
+func (t *GamePlayerMessageS2C) FromGamePlayerMessageS2CPrepare(v GamePlayerMessageS2CPrepare) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageS2CPrepare performs a merge with any union data inside the GamePlayerMessageS2C, using the provided GamePlayerMessageS2CPrepare
+func (t *GamePlayerMessageS2C) MergeGamePlayerMessageS2CPrepare(v GamePlayerMessageS2CPrepare) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+// AsGamePlayerMessageS2CStart returns the union data inside the GamePlayerMessageS2C as a GamePlayerMessageS2CStart
+func (t GamePlayerMessageS2C) AsGamePlayerMessageS2CStart() (GamePlayerMessageS2CStart, error) {
+ var body GamePlayerMessageS2CStart
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageS2CStart overwrites any union data inside the GamePlayerMessageS2C as the provided GamePlayerMessageS2CStart
+func (t *GamePlayerMessageS2C) FromGamePlayerMessageS2CStart(v GamePlayerMessageS2CStart) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageS2CStart performs a merge with any union data inside the GamePlayerMessageS2C, using the provided GamePlayerMessageS2CStart
+func (t *GamePlayerMessageS2C) MergeGamePlayerMessageS2CStart(v GamePlayerMessageS2CStart) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+// AsGamePlayerMessageS2CExecResult returns the union data inside the GamePlayerMessageS2C as a GamePlayerMessageS2CExecResult
+func (t GamePlayerMessageS2C) AsGamePlayerMessageS2CExecResult() (GamePlayerMessageS2CExecResult, error) {
+ var body GamePlayerMessageS2CExecResult
+ err := json.Unmarshal(t.union, &body)
+ return body, err
+}
+
+// FromGamePlayerMessageS2CExecResult overwrites any union data inside the GamePlayerMessageS2C as the provided GamePlayerMessageS2CExecResult
+func (t *GamePlayerMessageS2C) FromGamePlayerMessageS2CExecResult(v GamePlayerMessageS2CExecResult) error {
+ b, err := json.Marshal(v)
+ t.union = b
+ return err
+}
+
+// MergeGamePlayerMessageS2CExecResult performs a merge with any union data inside the GamePlayerMessageS2C, using the provided GamePlayerMessageS2CExecResult
+func (t *GamePlayerMessageS2C) MergeGamePlayerMessageS2CExecResult(v GamePlayerMessageS2CExecResult) error {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return err
+ }
+
+ merged, err := runtime.JSONMerge(t.union, b)
+ t.union = merged
+ return err
+}
+
+func (t GamePlayerMessageS2C) MarshalJSON() ([]byte, error) {
+ b, err := t.union.MarshalJSON()
+ return b, err
+}
+
+func (t *GamePlayerMessageS2C) UnmarshalJSON(b []byte) error {
+ err := t.union.UnmarshalJSON(b)
+ return err
+}
+
// ServerInterface represents all server handlers.
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
@@ -128,6 +452,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
@@ -166,6 +528,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)
}
@@ -200,6 +563,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
}
@@ -235,6 +627,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)
@@ -277,6 +672,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
@@ -309,20 +730,26 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/6xUTW/jNhD9K8K0R8FW4iDY+pai6CKLPRhoe1oExlgcS0wlkssZJesG+u8FqQ9btoqk",
- "SHKIZGo+3nvzOC+Q29pZQ0YY1i/AeUk1xtfPWFN4Om8dedEUT5VmV+Fha/qv9ANrVxGsY3xyBSnIwYXf",
- "LF6bAtoUVONRtDVbptwaxZO81W02pmgjVJAPOQXWtNVqEno1F+i83VVUh8CfPe1hDT8tj5yWPaHlpg9r",
- "U2BBL6S2KJPqv9zc3n66+ZTNwmFB6fiapob1N8gry6QghWfUok2xJSM+aHQ8iX0gICSHnqDvHESJ/LqX",
- "vTaaS1LwkJ6IibnoJ7oUs03B0/dGe1IBxaDSADCdzmdG+oexpN09Ui6B3Jdn2eChsqj+z7y/2NIkv1ma",
- "m7jOrdk6lHKastQ1FsTLR1uaxaMrZlN5i6rWZpK5x4ppDN5ZWxGaEN0w+QubXK/mRhhCL1kEKK/KPHQ5",
- "KXKh9Ih7TuHN0aRn8hLnXrswoimuP0vNieYEk8HgM1r1n950T0RLdca9RzV3ac8EOGk0VEon2C9JhxLa",
- "7G1o2feGu2qH4i1zEoB5g1XyTLvkbnMPKTyR5ygDZIurRRYwW0cGnYY1rBbZIgt3CaWMwi2D9eNbQfEe",
- "B1Wj1e9VWEYkn2NASPFYk5BnWH97geAs+N6QP0AKnR8gzHGYcLcwIuozDdu0zy4JFflj+l0jpfX6n9ge",
- "TpUT39BMyVHlhxDMzhruuFxnWXjk1giZSAudq3QeKy8fuXPJsd7UTKMkWqjm1zZi3O/tODf0Hg+zC4b/",
- "Y7oT78JXzZLYfdJltCncZKt3cKmJGYszw/5u/U4rRSYZp/2qdYdCb+Ew1o9VuKlr9IeBW0+sTWFZ2aJb",
- "UM7yjPk2luVrDOmgEMuvVh3eoYZD5mfrp9d8PL26Xs1th3cuvH6vja3nBZx6vf1QP4v9m87W4o/wtzj5",
- "/yqVrshbpv9Hk+fEvG+q6pBgIyUZCVBJdXa++mg735snrLRKck8q9MKKP9TOQ/1hmon1yTjOqcP/YvJJ",
- "Z+u2bdt/AwAA//+RpToKFwoAAA==",
+ "H4sIAAAAAAAC/9xX3W7jNhN9FX38CmwLCP7LItj6Lk23aRZb1Fi3V4vAoMWxzZQitRxqEzfQuxf8kWRZ",
+ "8kpJfFE0F44tcYZnzpwhZ55IotJMSZAGyfyJYLKDlLqvNzQF+z/TKgNtOLinjGMm6H4lw1t4pGkmgMzd",
+ "+mhKYmL2mf2NRnO5JUVMWK6p4UquEBIlGTbsLi4nlQmXBragrc2WprDirLF02rUw02otILULv9OwIXPy",
+ "/3Ed0zgENF6EZUVM0FBtgK2oaXj/8e3l5bu37yadcNBQ4+OVeUrmn0kiFAIjMXmg3HC5XYE02nJUP3H7",
+ "EIsQMqqBhJ0tKS4+/2XDJccdMHIXH5BZuT8is4iJhi8518AsipKlEmDczE8H9XeVS7W+h8TY4GzmFoLu",
+ "Qf8GiHTrAlUSft+Q+edv09oyXc6uSRE/0+h6tiTFXRcS++blYK5ny/fS6P2LEH0Cyl5mea0YnI7HvW3X",
+ "FTW0T8OnvC3oXijKbCp9bm1VSzRkTjK3fJ7McJ7YffsE5d7GHs0gqRxBaMWVhGhraWeaS/P9m19BCBVH",
+ "D0oL9r83P/Qic46GQvJZb4H5BjvgLAbRMxSEF9BzQGhncT4QthhfVcqLcHQ9uwqWs+ulO/5eYvn+EZJP",
+ "gLkwJ6qoueY8tdTw2V9ROEvm8AiJ9hjOXledcFqRYqJ0s7ym9v6SuRB0bX8ancOp+yzHwwsN8yQBxOY1",
+ "VD7sCy+4iwOgoRGW8jpbBoPDYemr7+Xz5+4ISCvA53YtR5BK86FwfC2ejWbnbhjJZRN0foobINqVYd8+",
+ "o8VrC9qbd6H58HB639P98Qe1k9HPCro6ZJ4oucqo2TVNxjylW8DxvdrJ0X227TTFFWUplw3LDRVY1/1a",
+ "KQFU2tU5gm611bOLriPCLm1HYaH0prPc5cBJqzOtcHcxvKjL44hewETzzLa0TVx/7DhGHCMalbXRwVV4",
+ "NWiuMNyIo9gDqq4hp7s+PQfeU9zA3g7auuByo1yL4PcmV2JNjVaIkQWmJRXRA6yjq8UticlX0OhoIJPR",
+ "dDSxmFUGkmaczMnFaDKa2NmDmp0jbmxHBfdtC64oLKtuNLhldngDc+MWWBNNUzCg0bUMVlnkSw6uO/J6",
+ "CPUdBg93TtSNzWFJBesdUAa6Nr/KzU5p/rfbnhwy56+rlsuK5Tu7GDMl0ccym0zCuWNAurBolgmeOM/j",
+ "e/Qqqf01xVRRwg2kOOQgrA87QrWm+86BDE9kt6Fd8pGjidQm8hZFTN5OLl4RS1pPbrVgf1F6zRkDGVXZ",
+ "7pVu6WhIDJV/5wXzNKW27faxhcCKOGhv/BSm1aJXhfbjlp3QojsmKy3VE3Cviv7NwuwXXpv9K0fxf0w6",
+ "N2AiGgKz0hFq6++2TGGHYhYKzUe3xEMBND8pP3W9kI2MIj4ozY6G1vB0OrvoulheeVeGK7HaupvAphqL",
+ "sx6FRv0FRzfqo/0bHXz2d3HOyZDsL/1cscmF2Ec0NzuQxkIF5uU8Pbecb+VXKjiLEg3M7kUFnlXOpf8y",
+ "m5HSUZXOpsL/RNCRl3VRFMU/AQAA///TQodrghUAAA==",
}
// 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..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()
diff --git a/backend/main.go b/backend/main.go
index 379fe8d..91caa73 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -6,7 +6,7 @@ import (
"log"
"net/http"
- "github.com/jackc/pgx/v5"
+ "github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
oapimiddleware "github.com/oapi-codegen/echo-middleware"
@@ -16,6 +16,19 @@ import (
"github.com/nsfisis/iosdc-2024-albatross/backend/game"
)
+func connectDB(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
+ pool, err := pgxpool.New(ctx, dsn)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := pool.Ping(ctx); err != nil {
+ return nil, err
+ }
+
+ return pool, nil
+}
+
func main() {
var err error
config, err := NewConfigFromEnv()
@@ -30,13 +43,14 @@ func main() {
ctx := context.Background()
- conn, err := pgx.Connect(ctx, fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", config.dbHost, config.dbPort, config.dbUser, config.dbPassword, config.dbName))
+ dbDSN := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", config.dbHost, config.dbPort, config.dbUser, config.dbPassword, config.dbName)
+ connPool, err := connectDB(ctx, dbDSN)
if err != nil {
log.Fatalf("Error connecting to db %v", err)
}
- defer conn.Close(ctx)
+ defer connPool.Close()
- queries := db.New(conn)
+ queries := db.New(connPool)
e := echo.New()
@@ -50,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;