aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-12 05:54:49 +0900
committernsfisis <nsfisis@gmail.com>2024-08-12 05:54:49 +0900
commit3074f8d74330a2c238040755b758230d682a4bc4 (patch)
tree3c45267ee25aa78be1ad4c31e0c09894e656b506
parent7527e54bba0c528015ce402bfa4534c1ab6ca1da (diff)
parentb37d6f213c2f3b19631e5067f39a7106859faaed (diff)
downloadiosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.gz
iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.tar.zst
iosdc-japan-2024-albatross-3074f8d74330a2c238040755b758230d682a4bc4.zip
Merge branch 'feat/play-page'
-rw-r--r--backend/admin/handler.go20
-rw-r--r--backend/api/generated.go164
-rw-r--r--backend/api/handler.go21
-rw-r--r--backend/db/query.sql.go13
-rw-r--r--backend/game/hub.go296
-rw-r--r--backend/game/message.go11
-rw-r--r--backend/query.sql5
-rw-r--r--frontend/app/.server/api/schema.d.ts30
-rw-r--r--frontend/app/components/ExecStatusIndicatorIcon.tsx8
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx99
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx117
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx104
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx140
-rw-r--r--frontend/app/components/SubmitStatusLabel.tsx26
-rw-r--r--frontend/app/models/ExecResult.ts18
-rw-r--r--frontend/app/models/PlayerInfo.ts9
-rw-r--r--frontend/app/models/SubmitResult.ts16
-rw-r--r--frontend/app/routes/_index.tsx2
-rw-r--r--frontend/app/routes/dashboard.tsx4
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx7
-rw-r--r--frontend/app/tailwind.css6
-rw-r--r--openapi/api-server.yaml62
-rw-r--r--worker/exec.go11
-rw-r--r--worker/models.go1
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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",
+ "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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"