diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-08-12 05:54:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-08-12 05:54:49 +0900 |
| commit | 3074f8d74330a2c238040755b758230d682a4bc4 (patch) | |
| tree | 3c45267ee25aa78be1ad4c31e0c09894e656b506 | |
| parent | 7527e54bba0c528015ce402bfa4534c1ab6ca1da (diff) | |
| parent | b37d6f213c2f3b19631e5067f39a7106859faaed (diff) | |
| download | iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.gz iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.zst iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.zip | |
Merge branch 'feat/play-page'
24 files changed, 728 insertions, 462 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go index 5398107..41eacd4 100644 --- a/backend/admin/handler.go +++ b/backend/admin/handler.go @@ -229,16 +229,6 @@ func (h *Handler) postGameEdit(c echo.Context) error { } } - { - // TODO: - if state != row.State && state == "starting" { - err := h.hubs.StartGame(int(gameID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } - } - err = h.q.UpdateGame(c.Request().Context(), db.UpdateGameParams{ GameID: int32(gameID), GameType: gameType, @@ -252,5 +242,15 @@ func (h *Handler) postGameEdit(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } + { + // TODO: + if state != row.State && state == "starting" { + err := h.hubs.StartGame(int(gameID)) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + } + } + return c.Redirect(http.StatusSeeOther, c.Request().URL.Path) } diff --git a/backend/api/generated.go b/backend/api/generated.go index ac536c0..7eb5940 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -40,13 +40,23 @@ const ( // Defines values for GamePlayerMessageS2CExecResultPayloadStatus. const ( GamePlayerMessageS2CExecResultPayloadStatusCompileError GamePlayerMessageS2CExecResultPayloadStatus = "compile_error" - GamePlayerMessageS2CExecResultPayloadStatusFailure GamePlayerMessageS2CExecResultPayloadStatus = "failure" GamePlayerMessageS2CExecResultPayloadStatusInternalError GamePlayerMessageS2CExecResultPayloadStatus = "internal_error" + GamePlayerMessageS2CExecResultPayloadStatusRuntimeError GamePlayerMessageS2CExecResultPayloadStatus = "runtime_error" GamePlayerMessageS2CExecResultPayloadStatusSuccess GamePlayerMessageS2CExecResultPayloadStatus = "success" GamePlayerMessageS2CExecResultPayloadStatusTimeout GamePlayerMessageS2CExecResultPayloadStatus = "timeout" GamePlayerMessageS2CExecResultPayloadStatusWrongAnswer GamePlayerMessageS2CExecResultPayloadStatus = "wrong_answer" ) +// Defines values for GamePlayerMessageS2CSubmitResultPayloadStatus. +const ( + GamePlayerMessageS2CSubmitResultPayloadStatusCompileError GamePlayerMessageS2CSubmitResultPayloadStatus = "compile_error" + GamePlayerMessageS2CSubmitResultPayloadStatusInternalError GamePlayerMessageS2CSubmitResultPayloadStatus = "internal_error" + GamePlayerMessageS2CSubmitResultPayloadStatusRuntimeError GamePlayerMessageS2CSubmitResultPayloadStatus = "runtime_error" + GamePlayerMessageS2CSubmitResultPayloadStatusSuccess GamePlayerMessageS2CSubmitResultPayloadStatus = "success" + GamePlayerMessageS2CSubmitResultPayloadStatusTimeout GamePlayerMessageS2CSubmitResultPayloadStatus = "timeout" + GamePlayerMessageS2CSubmitResultPayloadStatusWrongAnswer GamePlayerMessageS2CSubmitResultPayloadStatus = "wrong_answer" +) + // Defines values for GameWatcherMessageS2CExecResultPayloadStatus. const ( GameWatcherMessageS2CExecResultPayloadStatusCompileError GameWatcherMessageS2CExecResultPayloadStatus = "compile_error" @@ -59,12 +69,12 @@ const ( // Defines values for GameWatcherMessageS2CSubmitResultPayloadStatus. const ( - CompileError GameWatcherMessageS2CSubmitResultPayloadStatus = "compile_error" - InternalError GameWatcherMessageS2CSubmitResultPayloadStatus = "internal_error" - RuntimeError GameWatcherMessageS2CSubmitResultPayloadStatus = "runtime_error" - Success GameWatcherMessageS2CSubmitResultPayloadStatus = "success" - Timeout GameWatcherMessageS2CSubmitResultPayloadStatus = "timeout" - WrongAnswer GameWatcherMessageS2CSubmitResultPayloadStatus = "wrong_answer" + GameWatcherMessageS2CSubmitResultPayloadStatusCompileError GameWatcherMessageS2CSubmitResultPayloadStatus = "compile_error" + GameWatcherMessageS2CSubmitResultPayloadStatusInternalError GameWatcherMessageS2CSubmitResultPayloadStatus = "internal_error" + GameWatcherMessageS2CSubmitResultPayloadStatusRuntimeError GameWatcherMessageS2CSubmitResultPayloadStatus = "runtime_error" + GameWatcherMessageS2CSubmitResultPayloadStatusSuccess GameWatcherMessageS2CSubmitResultPayloadStatus = "success" + GameWatcherMessageS2CSubmitResultPayloadStatusTimeout GameWatcherMessageS2CSubmitResultPayloadStatus = "timeout" + GameWatcherMessageS2CSubmitResultPayloadStatusWrongAnswer GameWatcherMessageS2CSubmitResultPayloadStatus = "wrong_answer" ) // Error defines model for Error. @@ -72,17 +82,23 @@ type Error struct { Message string `json:"message"` } +// ExecStep defines model for ExecStep. +type ExecStep struct { + Label string `json:"label"` + TestcaseID nullable.Nullable[int] `json:"testcase_id"` +} + // Game defines model for Game. type Game struct { - DisplayName string `json:"display_name"` - DurationSeconds int `json:"duration_seconds"` - GameID int `json:"game_id"` - GameType GameGameType `json:"game_type"` - Players []User `json:"players"` - Problem Problem `json:"problem"` - StartedAt *int `json:"started_at,omitempty"` - State GameState `json:"state"` - VerificationSteps []VerificationStep `json:"verification_steps"` + DisplayName string `json:"display_name"` + DurationSeconds int `json:"duration_seconds"` + ExecSteps []ExecStep `json:"exec_steps"` + GameID int `json:"game_id"` + GameType GameGameType `json:"game_type"` + Players []User `json:"players"` + Problem Problem `json:"problem"` + StartedAt *int64 `json:"started_at,omitempty"` + State GameState `json:"state"` } // GameGameType defines model for Game.GameType. @@ -136,8 +152,10 @@ type GamePlayerMessageS2CExecResult struct { // GamePlayerMessageS2CExecResultPayload defines model for GamePlayerMessageS2CExecResultPayload. type GamePlayerMessageS2CExecResultPayload struct { - Score nullable.Nullable[int] `json:"score"` - Status GamePlayerMessageS2CExecResultPayloadStatus `json:"status"` + Status GamePlayerMessageS2CExecResultPayloadStatus `json:"status"` + Stderr string `json:"stderr"` + Stdout string `json:"stdout"` + TestcaseID nullable.Nullable[int] `json:"testcase_id"` } // GamePlayerMessageS2CExecResultPayloadStatus defines model for GamePlayerMessageS2CExecResultPayload.Status. @@ -151,9 +169,24 @@ type GamePlayerMessageS2CStart struct { // GamePlayerMessageS2CStartPayload defines model for GamePlayerMessageS2CStartPayload. type GamePlayerMessageS2CStartPayload struct { - StartAt int `json:"start_at"` + StartAt int64 `json:"start_at"` +} + +// GamePlayerMessageS2CSubmitResult defines model for GamePlayerMessageS2CSubmitResult. +type GamePlayerMessageS2CSubmitResult struct { + Data GamePlayerMessageS2CSubmitResultPayload `json:"data"` + Type string `json:"type"` } +// GamePlayerMessageS2CSubmitResultPayload defines model for GamePlayerMessageS2CSubmitResultPayload. +type GamePlayerMessageS2CSubmitResultPayload struct { + Score nullable.Nullable[int] `json:"score"` + Status GamePlayerMessageS2CSubmitResultPayloadStatus `json:"status"` +} + +// GamePlayerMessageS2CSubmitResultPayloadStatus defines model for GamePlayerMessageS2CSubmitResultPayload.Status. +type GamePlayerMessageS2CSubmitResultPayloadStatus string + // GameWatcherMessage defines model for GameWatcherMessage. type GameWatcherMessage struct { union json.RawMessage @@ -202,7 +235,7 @@ type GameWatcherMessageS2CStart struct { // GameWatcherMessageS2CStartPayload defines model for GameWatcherMessageS2CStartPayload. type GameWatcherMessageS2CStartPayload struct { - StartAt int `json:"start_at"` + StartAt int64 `json:"start_at"` } // GameWatcherMessageS2CSubmit defines model for GameWatcherMessageS2CSubmit. @@ -213,8 +246,7 @@ type GameWatcherMessageS2CSubmit struct { // GameWatcherMessageS2CSubmitPayload defines model for GameWatcherMessageS2CSubmitPayload. type GameWatcherMessageS2CSubmitPayload struct { - PlayerID int `json:"player_id"` - PreliminaryScore int `json:"preliminary_score"` + PlayerID int `json:"player_id"` } // GameWatcherMessageS2CSubmitResult defines model for GameWatcherMessageS2CSubmitResult. @@ -226,6 +258,7 @@ type GameWatcherMessageS2CSubmitResult struct { // GameWatcherMessageS2CSubmitResultPayload defines model for GameWatcherMessageS2CSubmitResultPayload. type GameWatcherMessageS2CSubmitResultPayload struct { PlayerID int `json:"player_id"` + Score nullable.Nullable[int] `json:"score"` Status GameWatcherMessageS2CSubmitResultPayloadStatus `json:"status"` } @@ -248,12 +281,6 @@ type User struct { Username string `json:"username"` } -// VerificationStep defines model for VerificationStep. -type VerificationStep struct { - Label string `json:"label"` - TestcaseID nullable.Nullable[int] `json:"testcase_id"` -} - // HeaderAuthorization defines model for header_authorization. type HeaderAuthorization = string @@ -473,6 +500,32 @@ func (t *GamePlayerMessageS2C) MergeGamePlayerMessageS2CExecResult(v GamePlayerM return err } +// AsGamePlayerMessageS2CSubmitResult returns the union data inside the GamePlayerMessageS2C as a GamePlayerMessageS2CSubmitResult +func (t GamePlayerMessageS2C) AsGamePlayerMessageS2CSubmitResult() (GamePlayerMessageS2CSubmitResult, error) { + var body GamePlayerMessageS2CSubmitResult + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromGamePlayerMessageS2CSubmitResult overwrites any union data inside the GamePlayerMessageS2C as the provided GamePlayerMessageS2CSubmitResult +func (t *GamePlayerMessageS2C) FromGamePlayerMessageS2CSubmitResult(v GamePlayerMessageS2CSubmitResult) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeGamePlayerMessageS2CSubmitResult performs a merge with any union data inside the GamePlayerMessageS2C, using the provided GamePlayerMessageS2CSubmitResult +func (t *GamePlayerMessageS2C) MergeGamePlayerMessageS2CSubmitResult(v GamePlayerMessageS2CSubmitResult) 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 @@ -1108,33 +1161,34 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 4150ba6..a0ecd4c 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -84,9 +84,9 @@ func (h *Handler) GetGames(ctx context.Context, _ GetGamesRequestObject, user *a } games := make([]Game, len(gameRows)) for i, row := range gameRows { - var startedAt *int + var startedAt *int64 if row.StartedAt.Valid { - startedAtTimestamp := int(row.StartedAt.Time.Unix()) + startedAtTimestamp := row.StartedAt.Time.Unix() startedAt = &startedAtTimestamp } games[i] = Game{ @@ -123,9 +123,9 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - var startedAt *int + var startedAt *int64 if row.StartedAt.Valid { - startedAtTimestamp := int(row.StartedAt.Time.Unix()) + startedAtTimestamp := row.StartedAt.Time.Unix() startedAt = &startedAtTimestamp } playerRows, err := h.q.ListGamePlayers(ctx, int32(gameID)) @@ -146,12 +146,13 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - verificationSteps := make([]VerificationStep, len(testcaseIDs)+1) - verificationSteps[0] = VerificationStep{ - Label: "Compile", + execSteps := make([]ExecStep, len(testcaseIDs)+1) + execSteps[0] = ExecStep{ + TestcaseID: nullable.NewNullNullable[int](), + Label: "Compile", } for i, testcaseID := range testcaseIDs { - verificationSteps[i+1] = VerificationStep{ + execSteps[i+1] = ExecStep{ TestcaseID: nullable.NewNullableWithValue(int(testcaseID)), Label: fmt.Sprintf("Testcase %d", i+1), } @@ -168,8 +169,8 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use Title: row.Title, Description: row.Description, }, - Players: players, - VerificationSteps: verificationSteps, + Players: players, + ExecSteps: execSteps, } return GetGame200JSONResponse{ Game: game, diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 583389e..34b0ae9 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -174,6 +174,19 @@ func (q *Queries) GetGameByID(ctx context.Context, gameID int32) (GetGameByIDRow return i, err } +const getSubmissionCodeSizeByID = `-- name: GetSubmissionCodeSizeByID :one +SELECT code_size FROM submissions +WHERE submission_id = $1 +LIMIT 1 +` + +func (q *Queries) GetSubmissionCodeSizeByID(ctx context.Context, submissionID int32) (int32, error) { + row := q.db.QueryRow(ctx, getSubmissionCodeSizeByID, submissionID) + var code_size int32 + err := row.Scan(&code_size) + return code_size, 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 diff --git a/backend/game/hub.go b/backend/game/hub.go index 97303ec..7c06e05 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "regexp" "strings" "time" @@ -86,7 +87,7 @@ func (hub *gameHub) run() { // TODO: assert game state is gaming log.Printf("submit: %v", message.message) code := msg.Data.Code - codeSize := len(code) // TODO: exclude whitespaces. + codeSize := calcCodeSize(code) codeHash := calcHash(code) if err := hub.taskQueue.EnqueueTaskCreateSubmissionRecord( hub.game.gameID, @@ -101,8 +102,7 @@ func (hub *gameHub) run() { hub.broadcastToWatchers(&watcherMessageS2CSubmit{ Type: watcherMessageTypeS2CSubmit, Data: watcherMessageS2CSubmitPayload{ - PlayerID: message.client.playerID, - PreliminaryScore: codeSize, + PlayerID: message.client.playerID, }, }) default: @@ -138,6 +138,55 @@ func (hub *gameHub) run() { } } +func (hub *gameHub) sendExecResultMessage(playerID int, testcaseID nullable.Nullable[int], status string, stdout string, stderr string) { + hub.sendToPlayer(playerID, &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + TestcaseID: testcaseID, + Status: api.GamePlayerMessageS2CExecResultPayloadStatus(status), + Stdout: stdout, + Stderr: stderr, + }, + }) + hub.broadcastToWatchers(&watcherMessageS2CExecResult{ + Type: watcherMessageTypeS2CExecResult, + Data: watcherMessageS2CExecResultPayload{ + PlayerID: playerID, + TestcaseID: testcaseID, + Status: api.GameWatcherMessageS2CExecResultPayloadStatus(status), + Stdout: stdout, + Stderr: stderr, + }, + }) +} + +func (hub *gameHub) sendSubmitResult(playerID int, status string, score nullable.Nullable[int]) { + hub.sendToPlayer(playerID, &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(status), + Score: score, + }, + }) + hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ + Type: watcherMessageTypeS2CSubmitResult, + Data: watcherMessageS2CSubmitResultPayload{ + PlayerID: playerID, + Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(status), + Score: score, + }, + }) +} + +func (hub *gameHub) sendToPlayer(playerID int, msg playerMessageS2C) { + for player := range hub.players { + if player.playerID == playerID { + player.s2cMessages <- msg + return + } + } +} + func (hub *gameHub) broadcastToWatchers(msg watcherMessageS2C) { for watcher := range hub.watchers { watcher.s2cMessages <- msg @@ -160,100 +209,52 @@ func (hub *gameHub) processTaskResults() { case *taskqueue.TaskResultCreateSubmissionRecord: err := hub.processTaskResultCreateSubmissionRecord(taskResult) if err != nil { - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ - Type: playerMessageTypeS2CExecResult, - Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus(err.Status), - }, - } - } - hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ - Type: watcherMessageTypeS2CSubmitResult, - Data: watcherMessageS2CSubmitResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status), - }, - }) + hub.sendSubmitResult( + taskResult.TaskPayload.UserID(), + err.Status, + nullable.NewNullNullable[int](), + ) } case *taskqueue.TaskResultCompileSwiftToWasm: err := hub.processTaskResultCompileSwiftToWasm(taskResult) if err != nil { - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ - Type: playerMessageTypeS2CExecResult, - Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus(err.Status), - }, - } - } - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus(err.Status), - Stdout: err.Stdout, - Stderr: err.Stderr, - }, - }) - hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ - Type: watcherMessageTypeS2CSubmitResult, - Data: watcherMessageS2CSubmitResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status), - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullNullable[int](), + err.Status, + err.Stdout, + err.Stderr, + ) + hub.sendSubmitResult( + taskResult.TaskPayload.UserID(), + err.Status, + nullable.NewNullNullable[int](), + ) } case *taskqueue.TaskResultCompileWasmToNativeExecutable: err := hub.processTaskResultCompileWasmToNativeExecutable(taskResult) if err != nil { - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ - Type: playerMessageTypeS2CExecResult, - Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus(err.Status), - }, - } - } - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus(err.Status), - Stdout: err.Stdout, - Stderr: err.Stderr, - }, - }) - hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ - Type: watcherMessageTypeS2CSubmitResult, - Data: watcherMessageS2CSubmitResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status), - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullNullable[int](), + err.Status, + err.Stdout, + err.Stderr, + ) + hub.sendSubmitResult( + taskResult.TaskPayload.UserID(), + err.Status, + nullable.NewNullNullable[int](), + ) } else { - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus("success"), - // TODO: inherit the command stdout/stderr. - Stdout: "Successfully compiled", - Stderr: "", - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullNullable[int](), + "success", + // TODO: inherit the command stdout/stderr. + "Successfully compiled", + "", + ) } case *taskqueue.TaskResultRunTestcase: // FIXME: error handling @@ -269,82 +270,52 @@ func (hub *gameHub) processTaskResults() { Stderr: "", }) if err != nil { - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ - Type: playerMessageTypeS2CExecResult, - Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus("internal_error"), - }, - } - } - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus("internal_error"), - // TODO: inherit the command stdout/stderr? - Stdout: "", - Stderr: "internal error", - }, - }) - hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ - Type: watcherMessageTypeS2CSubmitResult, - Data: watcherMessageS2CSubmitResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus("internal_error"), - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + "internal_error", + // TODO: inherit the command stdout/stderr? + "", + "internal error", + ) + hub.sendSubmitResult( + taskResult.TaskPayload.UserID(), + "internal_error", + nullable.NewNullNullable[int](), + ) continue } - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ - Type: playerMessageTypeS2CExecResult, - Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus(aggregatedStatus), - }, - } - } if err1 != nil { - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus(err1.Status), - Stdout: err1.Stdout, - Stderr: err1.Stderr, - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + aggregatedStatus, + err1.Stdout, + err1.Stderr, + ) } else { - hub.broadcastToWatchers(&watcherMessageS2CExecResult{ - Type: watcherMessageTypeS2CExecResult, - Data: watcherMessageS2CExecResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), - Status: api.GameWatcherMessageS2CExecResultPayloadStatus("success"), - // TODO: inherit the command stdout/stderr? - Stdout: "Testcase passed", - Stderr: "", - }, - }) + hub.sendExecResultMessage( + taskResult.TaskPayload.UserID(), + nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + "success", + // TODO: inherit the command stdout/stderr? + "Testcase passed", + "", + ) } if aggregatedStatus != "running" { - hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ - Type: watcherMessageTypeS2CSubmitResult, - Data: watcherMessageS2CSubmitResultPayload{ - PlayerID: taskResult.TaskPayload.UserID(), - Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(aggregatedStatus), - }, - }) + var score nullable.Nullable[int] + if aggregatedStatus == "success" { + codeSize, err := hub.q.GetSubmissionCodeSizeByID(hub.ctx, int32(taskResult.TaskPayload.SubmissionID)) + if err == nil { + score = nullable.NewNullableWithValue(int(codeSize)) + } + } + hub.sendSubmitResult( + taskResult.TaskPayload.UserID(), + aggregatedStatus, + score, + ) } default: panic("unexpected task result type") @@ -538,14 +509,14 @@ func (hub *gameHub) startGame() error { player.s2cMessages <- &playerMessageS2CStart{ Type: playerMessageTypeS2CStart, Data: playerMessageS2CStartPayload{ - StartAt: int(startAt.Unix()), + StartAt: startAt.Unix(), }, } } hub.broadcastToWatchers(&watcherMessageS2CStart{ Type: watcherMessageTypeS2CStart, Data: watcherMessageS2CStartPayload{ - StartAt: int(startAt.Unix()), + StartAt: startAt.Unix(), }, }) err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{ @@ -693,3 +664,8 @@ func isTestcaseResultCorrect(expectedStdout, actualStdout string) bool { func calcHash(code string) string { return fmt.Sprintf("%x", md5.Sum([]byte(code))) } + +func calcCodeSize(code string) int { + re := regexp.MustCompile(`\s+`) + return len(re.ReplaceAllString(code, "")) +} diff --git a/backend/game/message.go b/backend/game/message.go index 4877ac4..b535c1d 100644 --- a/backend/game/message.go +++ b/backend/game/message.go @@ -8,10 +8,11 @@ import ( ) const ( - playerMessageTypeS2CStart = "player:s2c:start" - playerMessageTypeS2CExecResult = "player:s2c:execresult" - playerMessageTypeC2SCode = "player:c2s:code" - playerMessageTypeC2SSubmit = "player:c2s:submit" + playerMessageTypeS2CStart = "player:s2c:start" + playerMessageTypeS2CExecResult = "player:s2c:execresult" + playerMessageTypeS2CSubmitResult = "player:s2c:submitresult" + playerMessageTypeC2SCode = "player:c2s:code" + playerMessageTypeC2SSubmit = "player:c2s:submit" ) type playerMessageC2SWithClient struct { @@ -24,6 +25,8 @@ type playerMessageS2CStart = api.GamePlayerMessageS2CStart type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload type playerMessageS2CExecResult = api.GamePlayerMessageS2CExecResult type playerMessageS2CExecResultPayload = api.GamePlayerMessageS2CExecResultPayload +type playerMessageS2CSubmitResult = api.GamePlayerMessageS2CSubmitResult +type playerMessageS2CSubmitResultPayload = api.GamePlayerMessageS2CSubmitResultPayload type playerMessageC2S = interface{} type playerMessageC2SCode = api.GamePlayerMessageC2SCode diff --git a/backend/query.sql b/backend/query.sql index 408bf2d..25eb4df 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -78,6 +78,11 @@ INSERT INTO submissions (game_id, user_id, code, code_size, code_hash) VALUES ($1, $2, $3, $4, $5) RETURNING submission_id; +-- name: GetSubmissionCodeSizeByID :one +SELECT code_size FROM submissions +WHERE submission_id = $1 +LIMIT 1; + -- name: ListTestcasesByGameID :many SELECT * FROM testcases WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 2d116b9..7fd612e 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -113,9 +113,9 @@ export interface components { started_at?: number; problem: components["schemas"]["Problem"]; players: components["schemas"]["User"][]; - verification_steps: components["schemas"]["VerificationStep"][]; + exec_steps: components["schemas"]["ExecStep"][]; }; - VerificationStep: { + ExecStep: { /** @example 1 */ testcase_id: number | null; /** @example Test case 1 */ @@ -130,7 +130,7 @@ export interface components { description: string; }; GamePlayerMessage: components["schemas"]["GamePlayerMessageS2C"] | components["schemas"]["GamePlayerMessageC2S"]; - GamePlayerMessageS2C: components["schemas"]["GamePlayerMessageS2CStart"] | components["schemas"]["GamePlayerMessageS2CExecResult"]; + GamePlayerMessageS2C: components["schemas"]["GamePlayerMessageS2CStart"] | components["schemas"]["GamePlayerMessageS2CExecResult"] | components["schemas"]["GamePlayerMessageS2CSubmitResult"]; GamePlayerMessageS2CStart: { /** @constant */ type: "player:s2c:start"; @@ -146,11 +146,29 @@ export interface components { data: components["schemas"]["GamePlayerMessageS2CExecResultPayload"]; }; GamePlayerMessageS2CExecResultPayload: { + /** @example 1 */ + testcase_id: number | null; + /** + * @example success + * @enum {string} + */ + status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; + /** @example Hello, world! */ + stdout: string; + /** @example */ + stderr: string; + }; + GamePlayerMessageS2CSubmitResult: { + /** @constant */ + type: "player:s2c:submitresult"; + data: components["schemas"]["GamePlayerMessageS2CSubmitResultPayload"]; + }; + GamePlayerMessageS2CSubmitResultPayload: { /** * @example success * @enum {string} */ - status: "success" | "failure" | "timeout" | "internal_error" | "compile_error" | "wrong_answer"; + status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; /** @example 100 */ score: number | null; }; @@ -203,8 +221,6 @@ export interface components { GameWatcherMessageS2CSubmitPayload: { /** @example 1 */ player_id: number; - /** @example 100 */ - preliminary_score: number; }; GameWatcherMessageS2CExecResult: { /** @constant */ @@ -239,6 +255,8 @@ export interface components { * @enum {string} */ status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; + /** @example 100 */ + score: number | null; }; }; responses: { diff --git a/frontend/app/components/ExecStatusIndicatorIcon.tsx b/frontend/app/components/ExecStatusIndicatorIcon.tsx index a76e957..5277bfa 100644 --- a/frontend/app/components/ExecStatusIndicatorIcon.tsx +++ b/frontend/app/components/ExecStatusIndicatorIcon.tsx @@ -1,17 +1,23 @@ import { faBan, + faCircle, faCircleCheck, faCircleExclamation, faRotate, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { ExecResultStatus } from "../models/ExecResult"; type Props = { - status: string; + status: ExecResultStatus; }; export default function ExecStatusIndicatorIcon({ status }: Props) { switch (status) { + case "waiting_submission": + return ( + <FontAwesomeIcon icon={faCircle} fixedWidth className="text-gray-400" /> + ); case "running": return ( <FontAwesomeIcon diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx index 4aebd52..d527e07 100644 --- a/frontend/app/components/GolfPlayApp.client.tsx +++ b/frontend/app/components/GolfPlayApp.client.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; import type { components } from "../.server/api/schema"; import useWebSocket, { ReadyState } from "../hooks/useWebSocket"; +import type { PlayerInfo } from "../models/PlayerInfo"; import GolfPlayAppConnecting from "./GolfPlayApps/GolfPlayAppConnecting"; import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished"; import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming"; @@ -12,14 +13,17 @@ type GamePlayerMessageS2C = components["schemas"]["GamePlayerMessageS2C"]; type GamePlayerMessageC2S = components["schemas"]["GamePlayerMessageC2S"]; type Game = components["schemas"]["Game"]; +type User = components["schemas"]["User"]; type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished"; export default function GolfPlayApp({ game, + player, sockToken, }: { game: Game; + player: User; sockToken: string; }) { const socketUrl = @@ -39,16 +43,17 @@ export default function GolfPlayApp({ const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null); useEffect(() => { - if (gameState === "starting" && startedAt !== null) { + if ( + (gameState === "starting" || gameState === "gaming") && + startedAt !== null + ) { const timer1 = setInterval(() => { setLeftTimeSeconds((prev) => { if (prev === null) { return null; } if (prev <= 1) { - clearInterval(timer1); setGameState("gaming"); - return 0; } return prev - 1; }); @@ -70,9 +75,21 @@ export default function GolfPlayApp({ } }, [gameState, startedAt, game.duration_seconds]); - const [currentScore, setCurrentScore] = useState<number | null>(null); - - const [lastExecStatus, setLastExecStatus] = useState<string | null>(null); + const [playerInfo, setPlayerInfo] = useState<Omit<PlayerInfo, "code">>({ + displayName: player.display_name, + iconPath: player.icon_path ?? null, + score: null, + submitResult: { + status: "waiting_submission", + execResults: game.exec_steps.map((r) => ({ + testcase_id: r.testcase_id, + status: "waiting_submission", + label: r.label, + stdout: "", + stderr: "", + })), + }, + }); const onCodeChange = useDebouncedCallback((code: string) => { console.log("player:c2s:code"); @@ -83,11 +100,26 @@ export default function GolfPlayApp({ }, 1000); const onCodeSubmit = useDebouncedCallback((code: string) => { + if (code === "") { + return; + } console.log("player:c2s:submit"); sendJsonMessage({ type: "player:c2s:submit", data: { code }, }); + setPlayerInfo((prev) => ({ + ...prev, + submitResult: { + status: "running", + execResults: prev.submitResult.execResults.map((r) => ({ + ...r, + status: "running", + stdout: "", + stderr: "", + })), + }, + })); }, 1000); if (readyState === ReadyState.UNINSTANTIATED) { @@ -117,14 +149,46 @@ export default function GolfPlayApp({ setGameState("starting"); } } else if (lastJsonMessage.type === "player:s2c:execresult") { + const { testcase_id, status, stdout, stderr } = lastJsonMessage.data; + setPlayerInfo((prev) => { + const ret = { ...prev }; + ret.submitResult = { + ...prev.submitResult, + execResults: prev.submitResult.execResults.map((r) => + r.testcase_id === testcase_id && r.status === "running" + ? { + ...r, + status, + stdout, + stderr, + } + : r, + ), + }; + return ret; + }); + } else if (lastJsonMessage.type === "player:s2c:submitresult") { const { status, score } = lastJsonMessage.data; - if ( - score !== null && - (currentScore === null || score < currentScore) - ) { - setCurrentScore(score); - } - setLastExecStatus(status); + setPlayerInfo((prev) => { + const ret = { ...prev }; + ret.submitResult = { + ...prev.submitResult, + status, + }; + if (status === "success") { + if (score) { + if (ret.score === null || score < ret.score) { + ret.score = score; + } + } + } else { + ret.submitResult.execResults = prev.submitResult.execResults.map( + (r) => + r.status === "running" ? { ...r, status: "canceled" } : r, + ); + } + return ret; + }); } } else { if (game.started_at) { @@ -133,7 +197,7 @@ export default function GolfPlayApp({ // The game has already started. if (gameState !== "gaming" && gameState !== "finished") { setStartedAt(game.started_at); - setLeftTimeSeconds(0); + setLeftTimeSeconds(game.started_at - nowSec); setGameState("gaming"); } } else { @@ -159,7 +223,6 @@ export default function GolfPlayApp({ lastJsonMessage, readyState, gameState, - currentScore, ]); if (gameState === "connecting") { @@ -171,12 +234,14 @@ export default function GolfPlayApp({ } else if (gameState === "gaming") { return ( <GolfPlayAppGaming + gameDisplayName={game.display_name} + gameDurationSeconds={game.duration_seconds} + leftTimeSeconds={leftTimeSeconds!} + playerInfo={playerInfo} problemTitle={game.problem.title} problemDescription={game.problem.description} onCodeChange={onCodeChange} onCodeSubmit={onCodeSubmit} - currentScore={currentScore} - lastExecStatus={lastExecStatus} /> ); } else if (gameState === "finished") { diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index 31927a5..4730583 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,21 +1,33 @@ +import { faArrowDown } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link } from "@remix-run/react"; import React, { useRef } from "react"; +import SubmitButton from "../../components/SubmitButton"; +import type { PlayerInfo } from "../../models/PlayerInfo"; +import BorderedContainer from "../BorderedContainer"; +import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon"; +import SubmitStatusLabel from "../SubmitStatusLabel"; type Props = { + gameDisplayName: string; + gameDurationSeconds: number; + leftTimeSeconds: number; + playerInfo: Omit<PlayerInfo, "code">; problemTitle: string; problemDescription: string; onCodeChange: (code: string) => void; onCodeSubmit: (code: string) => void; - currentScore: number | null; - lastExecStatus: string | null; }; export default function GolfPlayAppGaming({ + gameDisplayName, + gameDurationSeconds, + leftTimeSeconds, + playerInfo, problemTitle, problemDescription, onCodeChange, onCodeSubmit, - currentScore, - lastExecStatus, }: Props) { const textareaRef = useRef<HTMLTextAreaElement>(null); @@ -29,36 +41,81 @@ export default function GolfPlayAppGaming({ } }; + const leftTime = (() => { + const k = gameDurationSeconds + leftTimeSeconds; + const m = Math.floor(k / 60); + const s = k % 60; + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + })(); + return ( - <div className="min-h-screen flex"> - <div className="mx-auto flex min-h-full flex-grow"> - <div className="flex w-1/2 flex-col justify-between p-4"> - <div> - <div className="mb-2 text-xl font-bold">{problemTitle}</div> - <div className="text-gray-700">{problemDescription}</div> - </div> - <div className="mb-4 mt-auto"> - <div className="mb-2"> - <div className="font-semibold text-green-500"> - Score: {currentScore ?? "-"} ({lastExecStatus ?? "-"}) - </div> - </div> - <button - onClick={handleSubmitButtonClick} - className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none" - > - Submit - </button> + <div className="min-h-screen bg-gray-100 flex flex-col"> + <div className="text-white bg-iosdc-japan flex flex-row justify-between px-4 py-2"> + <div className="font-bold"> + <div className="text-gray-100">{gameDisplayName}</div> + <div className="text-2xl">{leftTime}</div> + </div> + <div className="font-bold text-end"> + <Link to={"/dashboard"} className="text-gray-100"> + {playerInfo.displayName} + </Link> + <div className="text-2xl">{playerInfo.score}</div> + </div> + </div> + <div className="grow grid grid-cols-3 divide-x divide-gray-300"> + <div className="p-4"> + <div className="mb-2 text-xl font-bold">{problemTitle}</div> + <div className="p-2"> + <BorderedContainer> + <div className="text-gray-700">{problemDescription}</div> + </BorderedContainer> </div> </div> - <div className="w-1/2 p-4 flex"> - <div className="flex-grow"> - <textarea - ref={textareaRef} - onChange={handleTextChange} - className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500" - ></textarea> + <div className="p-4"> + <textarea + ref={textareaRef} + onChange={handleTextChange} + className="resize-none h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 transition duration-300" + ></textarea> + </div> + <div className="p-4 flex flex-col gap-4"> + <div className="flex"> + <SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton> + <div className="grow font-bold text-xl text-center m-1"> + <SubmitStatusLabel status={playerInfo.submitResult.status} /> + </div> </div> + <ul className="flex flex-col gap-2"> + {playerInfo.submitResult.execResults.map((r, idx) => ( + <li key={r.testcase_id ?? -1} className="flex gap-2"> + <div className="flex flex-col gap-2 p-2"> + <div className="w-6"> + <ExecStatusIndicatorIcon status={r.status} /> + </div> + {idx !== playerInfo.submitResult.execResults.length - 1 && ( + <div> + <FontAwesomeIcon + icon={faArrowDown} + fixedWidth + className="text-gray-500" + /> + </div> + )} + </div> + <div className="grow p-2 overflow-x-scroll"> + <BorderedContainer> + <div className="font-semibold">{r.label}</div> + <div> + <code> + {r.stdout} + {r.stderr} + </code> + </div> + </BorderedContainer> + </div> + </li> + ))} + </ul> </div> </div> </div> diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index 448a966..b2f3b69 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from "react"; import type { components } from "../.server/api/schema"; import useWebSocket, { ReadyState } from "../hooks/useWebSocket"; +import type { PlayerInfo } from "../models/PlayerInfo"; import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting"; import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished"; -import GolfWatchAppGaming, { - PlayerInfo, -} from "./GolfWatchApps/GolfWatchAppGaming"; +import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting"; @@ -40,16 +39,17 @@ export default function GolfWatchApp({ const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null); useEffect(() => { - if (gameState === "starting" && startedAt !== null) { + if ( + (gameState === "starting" || gameState === "gaming") && + startedAt !== null + ) { const timer1 = setInterval(() => { setLeftTimeSeconds((prev) => { if (prev === null) { return null; } if (prev <= 1) { - clearInterval(timer1); setGameState("gaming"); - return 0; } return prev - 1; }); @@ -79,14 +79,32 @@ export default function GolfWatchApp({ iconPath: playerA?.icon_path ?? null, score: null, code: "", - submissionResult: undefined, + submitResult: { + status: "waiting_submission", + execResults: game.exec_steps.map((r) => ({ + testcase_id: r.testcase_id, + status: "waiting_submission", + label: r.label, + stdout: "", + stderr: "", + })), + }, }); const [playerInfoB, setPlayerInfoB] = useState<PlayerInfo>({ displayName: playerB?.display_name ?? null, iconPath: playerB?.icon_path ?? null, score: null, code: "", - submissionResult: undefined, + submitResult: { + status: "waiting_submission", + execResults: game.exec_steps.map((r) => ({ + testcase_id: r.testcase_id, + status: "waiting_submission", + label: r.label, + stdout: "", + stderr: "", + })), + }, }); if (readyState === ReadyState.UNINSTANTIATED) { @@ -121,18 +139,16 @@ export default function GolfWatchApp({ player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; setter((prev) => ({ ...prev, code })); } else if (lastJsonMessage.type === "watcher:s2c:submit") { - const { player_id, preliminary_score } = lastJsonMessage.data; + const { player_id } = lastJsonMessage.data; const setter = player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; setter((prev) => ({ ...prev, - submissionResult: { + submitResult: { status: "running", - preliminaryScore: preliminary_score, - verificationResults: game.verification_steps.map((v) => ({ - testcase_id: v.testcase_id, + execResults: prev.submitResult.execResults.map((r) => ({ + ...r, status: "running", - label: v.label, stdout: "", stderr: "", })), @@ -145,50 +161,42 @@ export default function GolfWatchApp({ player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; setter((prev) => { const ret = { ...prev }; - if (ret.submissionResult === undefined) { - return ret; - } - ret.submissionResult = { - ...ret.submissionResult, - verificationResults: ret.submissionResult.verificationResults.map( - (v) => - v.testcase_id === testcase_id && v.status === "running" - ? { - ...v, - status, - stdout, - stderr, - } - : v, + ret.submitResult = { + ...prev.submitResult, + execResults: prev.submitResult.execResults.map((r) => + r.testcase_id === testcase_id && r.status === "running" + ? { + ...r, + status, + stdout, + stderr, + } + : r, ), }; return ret; }); } else if (lastJsonMessage.type === "watcher:s2c:submitresult") { - const { player_id, status } = lastJsonMessage.data; + const { player_id, status, score } = lastJsonMessage.data; const setter = player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; setter((prev) => { const ret = { ...prev }; - if (ret.submissionResult === undefined) { - return ret; - } - ret.submissionResult = { - ...ret.submissionResult, + ret.submitResult = { + ...prev.submitResult, status, }; if (status === "success") { - if ( - ret.score === null || - ret.submissionResult.preliminaryScore < ret.score - ) { - ret.score = ret.submissionResult.preliminaryScore; + if (score) { + if (ret.score === null || score < ret.score) { + ret.score = score; + } } } else { - ret.submissionResult.verificationResults = - ret.submissionResult.verificationResults.map((v) => - v.status === "running" ? { ...v, status: "canceled" } : v, - ); + ret.submitResult.execResults = prev.submitResult.execResults.map( + (r) => + r.status === "running" ? { ...r, status: "canceled" } : r, + ); } return ret; }); @@ -200,7 +208,7 @@ export default function GolfWatchApp({ // The game has already started. if (gameState !== "gaming" && gameState !== "finished") { setStartedAt(game.started_at); - setLeftTimeSeconds(0); + setLeftTimeSeconds(game.started_at - nowSec); setGameState("gaming"); } } else { @@ -221,7 +229,6 @@ export default function GolfWatchApp({ } } }, [ - game.verification_steps, game.started_at, lastJsonMessage, readyState, @@ -239,10 +246,11 @@ export default function GolfWatchApp({ } else if (gameState === "gaming") { return ( <GolfWatchAppGaming - problem={game.problem!.description} + gameDurationSeconds={game.duration_seconds} + leftTimeSeconds={leftTimeSeconds!} playerInfoA={playerInfoA} playerInfoB={playerInfoB} - leftTimeSeconds={leftTimeSeconds!} + problem={game.problem!.description} /> ); } else if (gameState === "finished") { diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 65cd35e..f9647b3 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,83 +1,29 @@ +import { PlayerInfo } from "../../models/PlayerInfo"; import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon"; +import SubmitStatusLabel from "../SubmitStatusLabel"; type Props = { - problem: string; + gameDurationSeconds: number; + leftTimeSeconds: number; playerInfoA: PlayerInfo; playerInfoB: PlayerInfo; - leftTimeSeconds: number; -}; - -export type PlayerInfo = { - displayName: string | null; - iconPath: string | null; - score: number | null; - code: string | null; - submissionResult?: SubmissionResult; -}; - -type SubmissionResult = { - status: - | "running" - | "success" - | "wrong_answer" - | "timeout" - | "compile_error" - | "runtime_error" - | "internal_error"; - preliminaryScore: number; - verificationResults: VerificationResult[]; -}; - -type VerificationResult = { - testcase_id: number | null; - status: - | "running" - | "success" - | "wrong_answer" - | "timeout" - | "compile_error" - | "runtime_error" - | "internal_error" - | "canceled"; - label: string; - stdout: string; - stderr: string; + problem: string; }; -function submissionResultStatusToLabel( - status: SubmissionResult["status"] | null, -) { - switch (status) { - case null: - return "-"; - case "running": - return "Running..."; - case "success": - return "Accepted"; - case "wrong_answer": - return "Wrong Answer"; - case "timeout": - return "Time Limit Exceeded"; - case "compile_error": - return "Compile Error"; - case "runtime_error": - return "Runtime Error"; - case "internal_error": - return "Internal Error"; - } -} - export default function GolfWatchAppGaming({ - problem, + gameDurationSeconds, + leftTimeSeconds, playerInfoA, playerInfoB, - leftTimeSeconds, + problem, }: Props) { const leftTime = (() => { - const m = Math.floor(leftTimeSeconds / 60); - const s = leftTimeSeconds % 60; + const k = gameDurationSeconds + leftTimeSeconds; + const m = Math.floor(k / 60); + const s = k % 60; return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; })(); + const scoreRatio = (() => { const scoreA = playerInfoA.score ?? 0; const scoreB = playerInfoB.score ?? 0; @@ -118,29 +64,24 @@ export default function GolfWatchAppGaming({ </div> <div> <div> - {submissionResultStatusToLabel( - playerInfoA.submissionResult?.status ?? null, - )}{" "} - ({playerInfoA.submissionResult?.preliminaryScore}) + <SubmitStatusLabel status={playerInfoA.submitResult.status} /> </div> <div> <ol> - {playerInfoA.submissionResult?.verificationResults.map( - (result) => ( - <li key={result.testcase_id ?? -1}> + {playerInfoA.submitResult?.execResults.map((result) => ( + <li key={result.testcase_id ?? -1}> + <div> + <div> + <ExecStatusIndicatorIcon status={result.status} />{" "} + {result.label} + </div> <div> - <div> - <ExecStatusIndicatorIcon status={result.status} />{" "} - {result.label} - </div> - <div> - {result.stdout} - {result.stderr} - </div> + {result.stdout} + {result.stderr} </div> - </li> - ), - )} + </div> + </li> + ))} </ol> </div> </div> @@ -151,29 +92,24 @@ export default function GolfWatchAppGaming({ </div> <div> <div> - {submissionResultStatusToLabel( - playerInfoB.submissionResult?.status ?? null, - )}{" "} - ({playerInfoB.submissionResult?.preliminaryScore ?? "-"}) + <SubmitStatusLabel status={playerInfoB.submitResult.status} /> </div> <div> <ol> - {playerInfoB.submissionResult?.verificationResults.map( - (result, idx) => ( - <li key={idx}> + {playerInfoB.submitResult?.execResults.map((result, idx) => ( + <li key={idx}> + <div> + <div> + <ExecStatusIndicatorIcon status={result.status} />{" "} + {result.label} + </div> <div> - <div> - <ExecStatusIndicatorIcon status={result.status} />{" "} - {result.label} - </div> - <div> - {result.stdout} - {result.stderr} - </div> + {result.stdout} + {result.stderr} </div> - </li> - ), - )} + </div> + </li> + ))} </ol> </div> </div> diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx new file mode 100644 index 0000000..e0ecc27 --- /dev/null +++ b/frontend/app/components/SubmitStatusLabel.tsx @@ -0,0 +1,26 @@ +import type { SubmitResultStatus } from "../models/SubmitResult"; + +type Props = { + status: SubmitResultStatus; +}; + +export default function SubmitStatusLabel({ status }: Props) { + switch (status) { + case "waiting_submission": + return null; + case "running": + return "実行中..."; + case "success": + return "成功"; + case "wrong_answer": + return "テスト失敗"; + case "timeout": + return "時間切れ"; + case "compile_error": + return "コンパイルエラー"; + case "runtime_error": + return "実行時エラー"; + case "internal_error": + return "!内部エラー!"; + } +} diff --git a/frontend/app/models/ExecResult.ts b/frontend/app/models/ExecResult.ts new file mode 100644 index 0000000..e0b6bb4 --- /dev/null +++ b/frontend/app/models/ExecResult.ts @@ -0,0 +1,18 @@ +export type ExecResultStatus = + | "waiting_submission" + | "running" + | "success" + | "wrong_answer" + | "timeout" + | "compile_error" + | "runtime_error" + | "internal_error" + | "canceled"; + +export type ExecResult = { + testcase_id: number | null; + status: ExecResultStatus; + label: string; + stdout: string; + stderr: string; +}; diff --git a/frontend/app/models/PlayerInfo.ts b/frontend/app/models/PlayerInfo.ts new file mode 100644 index 0000000..8092ab3 --- /dev/null +++ b/frontend/app/models/PlayerInfo.ts @@ -0,0 +1,9 @@ +import type { SubmitResult } from "./SubmitResult"; + +export type PlayerInfo = { + displayName: string | null; + iconPath: string | null; + score: number | null; + code: string | null; + submitResult: SubmitResult; +}; diff --git a/frontend/app/models/SubmitResult.ts b/frontend/app/models/SubmitResult.ts new file mode 100644 index 0000000..6df00b6 --- /dev/null +++ b/frontend/app/models/SubmitResult.ts @@ -0,0 +1,16 @@ +import type { ExecResult } from "./ExecResult"; + +export type SubmitResultStatus = + | "waiting_submission" + | "running" + | "success" + | "wrong_answer" + | "timeout" + | "compile_error" + | "runtime_error" + | "internal_error"; + +export type SubmitResult = { + status: SubmitResultStatus; + execResults: ExecResult[]; +}; diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index 9be594f..24fff9f 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -22,7 +22,7 @@ export default function Index() { className="w-24 h-24" /> <div className="text-center"> - <div className="font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400 flex flex-col gap-y-2"> + <div className="font-bold text-transparent bg-clip-text bg-iosdc-japan flex flex-col gap-y-2"> <div className="text-3xl">iOSDC Japan 2024</div> <div className="text-6xl">Swift Code Battle</div> </div> diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index 4c751d9..99c64f2 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -31,8 +31,8 @@ export default function Dashboard() { return ( <div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center"> - <h1 className="text-2xl font-bold mb-4 text-gray-800"> - <span>{user.display_name}</span> + <h1 className="text-2xl font-bold mb-4"> + <span className="text-gray-800">{user.display_name}</span> <span className="text-gray-500 ml-2">@{user.username}</span> </h1> <h2 className="text-xl font-semibold mb-4 text-gray-700">試合</h2> diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index 3712df2..ea1b8fd 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -15,7 +15,7 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => [ ]; export async function loader({ params, request }: LoaderFunctionArgs) { - const { token } = await ensureUserLoggedIn(request); + const { token, user } = await ensureUserLoggedIn(request); const fetchGame = async () => { return (await apiGetGame(token, Number(params.gameId))).game; @@ -27,16 +27,17 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); return { game, + player: user, sockToken, }; } export default function GolfPlay() { - const { game, sockToken } = useLoaderData<typeof loader>(); + const { game, player, sockToken } = useLoaderData<typeof loader>(); return ( <ClientOnly fallback={<GolfPlayAppConnecting />}> - {() => <GolfPlayApp game={game} sockToken={sockToken} />} + {() => <GolfPlayApp game={game} player={player} sockToken={sockToken} />} </ClientOnly> ); } diff --git a/frontend/app/tailwind.css b/frontend/app/tailwind.css index b5c61c9..6d3faee 100644 --- a/frontend/app/tailwind.css +++ b/frontend/app/tailwind.css @@ -1,3 +1,9 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer components { + .bg-iosdc-japan { + @apply bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400; + } +} diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml index 826c4ba..94083d3 100644 --- a/openapi/api-server.yaml +++ b/openapi/api-server.yaml @@ -212,16 +212,17 @@ components: started_at: type: integer example: 946684800 + x-go-type: int64 problem: $ref: '#/components/schemas/Problem' players: type: array items: $ref: '#/components/schemas/User' - verification_steps: + exec_steps: type: array items: - $ref: '#/components/schemas/VerificationStep' + $ref: '#/components/schemas/ExecStep' required: - game_id - game_type @@ -230,8 +231,8 @@ components: - duration_seconds - problem - players - - verification_steps - VerificationStep: + - exec_steps + ExecStep: type: object properties: testcase_id: @@ -268,6 +269,7 @@ components: oneOf: - $ref: '#/components/schemas/GamePlayerMessageS2CStart' - $ref: '#/components/schemas/GamePlayerMessageS2CExecResult' + - $ref: '#/components/schemas/GamePlayerMessageS2CSubmitResult' GamePlayerMessageS2CStart: type: object properties: @@ -285,6 +287,7 @@ components: start_at: type: integer example: 946684800 + x-go-type: int64 required: - start_at GamePlayerMessageS2CExecResult: @@ -301,16 +304,55 @@ components: GamePlayerMessageS2CExecResultPayload: type: object properties: + testcase_id: + type: integer + nullable: true + example: 1 status: type: string example: "success" enum: - success - - failure + - wrong_answer - timeout + - runtime_error - internal_error - compile_error + stdout: + type: string + example: "Hello, world!" + stderr: + type: string + example: "" + required: + - testcase_id + - status + - stdout + - stderr + GamePlayerMessageS2CSubmitResult: + type: object + properties: + type: + type: string + const: "player:s2c:submitresult" + data: + $ref: '#/components/schemas/GamePlayerMessageS2CSubmitResultPayload' + required: + - type + - data + GamePlayerMessageS2CSubmitResultPayload: + type: object + properties: + status: + type: string + example: "success" + enum: + - success - wrong_answer + - timeout + - runtime_error + - internal_error + - compile_error score: type: integer nullable: true @@ -388,6 +430,7 @@ components: start_at: type: integer example: 946684800 + x-go-type: int64 required: - start_at GameWatcherMessageS2CCode: @@ -430,12 +473,8 @@ components: player_id: type: integer example: 1 - preliminary_score: - type: integer - example: 100 required: - player_id - - preliminary_score GameWatcherMessageS2CExecResult: type: object properties: @@ -506,8 +545,13 @@ components: - runtime_error - internal_error - compile_error + score: + type: integer + nullable: true + example: 100 required: - player_id - status + - score # GameWatcherMessageC2S: # oneOf: diff --git a/worker/exec.go b/worker/exec.go index 9e937f7..37f542b 100644 --- a/worker/exec.go +++ b/worker/exec.go @@ -67,11 +67,14 @@ func execCommandWithTimeout( } } -func convertCommandErrorToResultType(err error) string { +func convertCommandErrorToResultType(err error, isCompile bool) string { if err != nil { if err == context.DeadlineExceeded { return resultTimeout } + if isCompile { + return resultCompileError + } return resultRuntimeError } return resultSuccess @@ -109,7 +112,7 @@ func execSwiftCompile( ) return swiftCompileResponseData{ - Status: convertCommandErrorToResultType(err), + Status: convertCommandErrorToResultType(err, true), Stdout: stdout, Stderr: stderr, } @@ -140,7 +143,7 @@ func execWasmCompile( ) return wasmCompileResponseData{ - Status: convertCommandErrorToResultType(err), + Status: convertCommandErrorToResultType(err, true), Stdout: stdout, Stderr: stderr, } @@ -170,7 +173,7 @@ func execTestRun( ) return testRunResponseData{ - Status: convertCommandErrorToResultType(err), + Status: convertCommandErrorToResultType(err, false), Stdout: stdout, Stderr: stderr, } diff --git a/worker/models.go b/worker/models.go index 9f60eb0..4a318d0 100644 --- a/worker/models.go +++ b/worker/models.go @@ -7,6 +7,7 @@ import ( const ( resultSuccess = "success" + resultCompileError = "compile_error" resultRuntimeError = "runtime_error" resultTimeout = "timeout" resultInternalError = "internal_error" |
