From 0b520dc2529d7df6263842480d4ba7eaf07f4dcd Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 14:06:28 +0900 Subject: feat(backend): include `players` in `Game` object --- backend/api/generated.go | 51 ++++++++++++++++++------------------ backend/api/handler.go | 15 +++++++++++ backend/db/query.sql.go | 10 +++---- backend/query.sql | 2 +- frontend/app/.server/api/schema.d.ts | 1 + openapi.yaml | 5 ++++ 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/backend/api/generated.go b/backend/api/generated.go index 33f1a78..2a113a4 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -65,6 +65,7 @@ type Game struct { DurationSeconds int `json:"duration_seconds"` GameID int `json:"game_id"` GameType GameGameType `json:"game_type"` + Players []User `json:"players"` Problem *Problem `json:"problem,omitempty"` StartedAt *int `json:"started_at,omitempty"` State GameState `json:"state"` @@ -1108,31 +1109,31 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error var swaggerSpec = []string{ "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", - "BmCWZdm/AQAA//8u6d2yGyIAAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 23c3cfe..8830d1c 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -135,6 +135,20 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use } } } + playerRows, err := h.q.ListGamePlayers(ctx, int32(gameID)) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + players := make([]User, len(playerRows)) + for i, playerRow := range playerRows { + players[i] = User{ + UserID: int(playerRow.UserID), + Username: playerRow.Username, + DisplayName: playerRow.DisplayName, + IconPath: playerRow.IconPath, + IsAdmin: playerRow.IsAdmin, + } + } game := Game{ GameID: int(row.GameID), GameType: GameGameType(row.GameType), @@ -143,6 +157,7 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use DurationSeconds: int(row.DurationSeconds), StartedAt: startedAt, Problem: problem, + Players: players, } return GetGame200JSONResponse{ Game: game, diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 47140fc..392e6a6 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -248,7 +248,7 @@ func (q *Queries) IsRegistrationTokenValid(ctx context.Context, token string) (b const listGamePlayers = `-- name: ListGamePlayers :many SELECT game_id, game_players.user_id, users.user_id, username, display_name, icon_path, is_admin, created_at FROM game_players -LEFT JOIN users ON game_players.user_id = users.user_id +JOIN users ON game_players.user_id = users.user_id WHERE game_players.game_id = $1 ORDER BY game_players.user_id ` @@ -256,11 +256,11 @@ ORDER BY game_players.user_id type ListGamePlayersRow struct { GameID int32 UserID int32 - UserID_2 *int32 - Username *string - DisplayName *string + UserID_2 int32 + Username string + DisplayName string IconPath *string - IsAdmin *bool + IsAdmin bool CreatedAt pgtype.Timestamp } diff --git a/backend/query.sql b/backend/query.sql index 13bbbe6..12b67b9 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -58,7 +58,7 @@ LIMIT 1; -- name: ListGamePlayers :many SELECT * FROM game_players -LEFT JOIN users ON game_players.user_id = users.user_id +JOIN users ON game_players.user_id = users.user_id WHERE game_players.game_id = $1 ORDER BY game_players.user_id; diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 9a96f19..f7caebb 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -112,6 +112,7 @@ export interface components { /** @example 946684800 */ started_at?: number; problem?: components["schemas"]["Problem"]; + players: components["schemas"]["User"][]; }; Problem: { /** @example 1 */ diff --git a/openapi.yaml b/openapi.yaml index ebad2f0..fdd0146 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -216,12 +216,17 @@ components: example: 946684800 problem: $ref: '#/components/schemas/Problem' + players: + type: array + items: + $ref: '#/components/schemas/User' required: - game_id - game_type - state - display_name - duration_seconds + - players Problem: type: object properties: -- cgit v1.2.3-70-g09d2 From 47f95342097e8828059053c168e06cd44e0ab43b Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 19:55:28 +0900 Subject: feat(backend): include `verification_steps` in `Game` object --- backend/api/generated.go | 76 ++++++++++++++++++++---------------- backend/api/handler.go | 33 ++++++++++++---- backend/db/query.sql.go | 26 ++++++++++++ backend/query.sql | 5 +++ frontend/app/.server/api/schema.d.ts | 7 ++++ openapi.yaml | 18 +++++++++ 6 files changed, 123 insertions(+), 42 deletions(-) diff --git a/backend/api/generated.go b/backend/api/generated.go index 2a113a4..b33747a 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -61,14 +61,15 @@ type Error struct { // 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,omitempty"` - StartedAt *int `json:"started_at,omitempty"` - State GameState `json:"state"` + 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,omitempty"` + StartedAt *int `json:"started_at,omitempty"` + State GameState `json:"state"` + VerificationSteps []VerificationStep `json:"verification_steps"` } // GameGameType defines model for Game.GameType. @@ -228,6 +229,12 @@ 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 @@ -1108,32 +1115,33 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 8830d1c..134a4f9 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -3,11 +3,13 @@ package api import ( "context" "errors" + "fmt" "log" "net/http" "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" + "github.com/oapi-codegen/nullable" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/auth" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" @@ -149,15 +151,30 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use IsAdmin: playerRow.IsAdmin, } } + testcaseIDs, err := h.q.ListTestcaseIDsByGameID(ctx, int32(gameID)) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + verificationSteps := make([]VerificationStep, len(testcaseIDs)+1) + verificationSteps[0] = VerificationStep{ + Label: "Compile", + } + for i, testcaseID := range testcaseIDs { + verificationSteps[i+1] = VerificationStep{ + TestcaseID: nullable.NewNullableWithValue(int(testcaseID)), + Label: fmt.Sprintf("Testcase %d", i+1), + } + } game := Game{ - GameID: int(row.GameID), - GameType: GameGameType(row.GameType), - State: GameState(row.State), - DisplayName: row.DisplayName, - DurationSeconds: int(row.DurationSeconds), - StartedAt: startedAt, - Problem: problem, - Players: players, + GameID: int(row.GameID), + GameType: GameGameType(row.GameType), + State: GameState(row.State), + DisplayName: row.DisplayName, + DurationSeconds: int(row.DurationSeconds), + StartedAt: startedAt, + Problem: problem, + Players: players, + VerificationSteps: verificationSteps, } return GetGame200JSONResponse{ Game: game, diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index 392e6a6..cbef51d 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -403,6 +403,32 @@ func (q *Queries) ListGamesForPlayer(ctx context.Context, userID int32) ([]ListG return items, nil } +const listTestcaseIDsByGameID = `-- name: ListTestcaseIDsByGameID :many +SELECT testcases.testcase_id FROM testcases +WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) +ORDER BY testcases.testcase_id +` + +func (q *Queries) ListTestcaseIDsByGameID(ctx context.Context, gameID int32) ([]int32, error) { + rows, err := q.db.Query(ctx, listTestcaseIDsByGameID, gameID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int32 + for rows.Next() { + var testcase_id int32 + if err := rows.Scan(&testcase_id); err != nil { + return nil, err + } + items = append(items, testcase_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listTestcasesByGameID = `-- name: ListTestcasesByGameID :many SELECT testcase_id, problem_id, stdin, stdout FROM testcases WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) diff --git a/backend/query.sql b/backend/query.sql index 12b67b9..bcbee12 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -83,6 +83,11 @@ SELECT * FROM testcases WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) ORDER BY testcases.testcase_id; +-- name: ListTestcaseIDsByGameID :many +SELECT testcases.testcase_id FROM testcases +WHERE testcases.problem_id = (SELECT problem_id FROM games WHERE game_id = $1) +ORDER BY testcases.testcase_id; + -- name: CreateSubmissionResult :exec INSERT INTO submission_results (submission_id, status, stdout, stderr) VALUES ($1, $2, $3, $4); diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index f7caebb..779f11e 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -113,6 +113,13 @@ export interface components { started_at?: number; problem?: components["schemas"]["Problem"]; players: components["schemas"]["User"][]; + verification_steps: components["schemas"]["VerificationStep"][]; + }; + VerificationStep: { + /** @example 1 */ + testcase_id: number | null; + /** @example Test case 1 */ + label: string; }; Problem: { /** @example 1 */ diff --git a/openapi.yaml b/openapi.yaml index fdd0146..6d9dea3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -220,6 +220,10 @@ components: type: array items: $ref: '#/components/schemas/User' + verification_steps: + type: array + items: + $ref: '#/components/schemas/VerificationStep' required: - game_id - game_type @@ -227,6 +231,20 @@ components: - display_name - duration_seconds - players + - verification_steps + VerificationStep: + type: object + properties: + testcase_id: + type: integer + nullable: true + example: 1 + label: + type: string + example: "Test case 1" + required: + - testcase_id + - label Problem: type: object properties: -- cgit v1.2.3-70-g09d2 From 9477788709127ffd5611caed0fa7ee191326ce00 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 13:31:52 +0900 Subject: feat(frontend): partially implement watch page --- frontend/app/components/GolfWatchApp.client.tsx | 66 +++++++--- .../GolfWatchApps/GolfWatchAppGaming.tsx | 137 +++++++++++++++++---- .../GolfWatchApps/GolfWatchAppStarting.tsx | 6 +- .../GolfWatchApps/GolfWatchAppWaiting.tsx | 8 +- 4 files changed, 171 insertions(+), 46 deletions(-) diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index 829f709..355f7e3 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -3,7 +3,9 @@ import useWebSocket, { ReadyState } from "react-use-websocket"; import type { components } from "../.server/api/schema"; import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting"; import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished"; -import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; +import GolfWatchAppGaming, { + PlayerInfo, +} from "./GolfWatchApps/GolfWatchAppGaming"; import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting"; @@ -34,12 +36,12 @@ export default function GolfWatchApp({ const [startedAt, setStartedAt] = useState(null); - const [timeLeftSeconds, setTimeLeftSeconds] = useState(null); + const [leftTimeSeconds, setLeftTimeSeconds] = useState(null); useEffect(() => { if (gameState === "starting" && startedAt !== null) { const timer1 = setInterval(() => { - setTimeLeftSeconds((prev) => { + setLeftTimeSeconds((prev) => { if (prev === null) { return null; } @@ -68,10 +70,23 @@ export default function GolfWatchApp({ } }, [gameState, startedAt, game.duration_seconds]); - const [scoreA, setScoreA] = useState(null); - const [scoreB, setScoreB] = useState(null); - const [codeA, setCodeA] = useState(""); - const [codeB, setCodeB] = useState(""); + const playerA = game.players[0]; + const playerB = game.players[1]; + + const [playerInfoA, setPlayerInfoA] = useState({ + displayName: playerA?.display_name ?? null, + iconPath: playerA?.icon_path ?? null, + score: null, + code: "", + submissionResult: undefined, + }); + const [playerInfoB, setPlayerInfoB] = useState({ + displayName: playerB?.display_name ?? null, + iconPath: playerB?.icon_path ?? null, + score: null, + code: "", + submissionResult: undefined, + }); if (readyState === ReadyState.UNINSTANTIATED) { throw new Error("WebSocket is not connected"); @@ -96,38 +111,51 @@ export default function GolfWatchApp({ const { start_at } = lastJsonMessage.data; setStartedAt(start_at); const nowSec = Math.floor(Date.now() / 1000); - setTimeLeftSeconds(start_at - nowSec); + setLeftTimeSeconds(start_at - nowSec); setGameState("starting"); } } else if (lastJsonMessage.type === "watcher:s2c:code") { const { player_id, code } = lastJsonMessage.data; - setCodeA(code); - } else if (lastJsonMessage.type === "watcher:s2c:execresult") { - const { score } = lastJsonMessage.data; - if (score !== null && (scoreA === null || score < scoreA)) { - setScoreA(score); + if (player_id === playerA?.user_id) { + setPlayerInfoA((prev) => ({ ...prev, code })); + } else if (player_id === playerB?.user_id) { + setPlayerInfoB((prev) => ({ ...prev, code })); + } else { + throw new Error("Unknown player_id"); } + } else if (lastJsonMessage.type === "watcher:s2c:execresult") { + // const { score } = lastJsonMessage.data; + // if (score !== null && (scoreA === null || score < scoreA)) { + // setScoreA(score); + // } } } else { setGameState("waiting"); } } - }, [lastJsonMessage, readyState, gameState, scoreA]); + }, [ + lastJsonMessage, + readyState, + gameState, + playerInfoA, + playerInfoB, + playerA?.user_id, + playerB?.user_id, + ]); if (gameState === "connecting") { return ; } else if (gameState === "waiting") { return ; } else if (gameState === "starting") { - return ; + return ; } else if (gameState === "gaming") { return ( ); } else if (gameState === "finished") { diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 22277f8..470a00c 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,41 +1,130 @@ type Props = { problem: string; - codeA: string; - scoreA: number | null; - codeB: string; - scoreB: number | null; + 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: string; + nextScore: number; + executionResults: ExecutionResult[]; +}; + +type ExecutionResult = { + status: string; + label: string; + output: string; }; export default function GolfWatchAppGaming({ problem, - codeA, - scoreA, - codeB, - scoreB, + playerInfoA, + playerInfoB, + leftTimeSeconds, }: Props) { + const leftTime = (() => { + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + })(); + const scoreRatio = (() => { + const scoreA = playerInfoA.score ?? 0; + const scoreB = playerInfoB.score ?? 0; + const totalScore = scoreA + scoreB; + return totalScore === 0 ? 50 : (scoreA / totalScore) * 100; + })(); + return ( -
-
-
{problem}
+
+
+
+ {playerInfoA.displayName} +
+
{leftTime}
+
+ {playerInfoB.displayName} +
-
-
-
{scoreA}
-
-
-							{codeA}
-						
+
+
+ {playerInfoA.score ?? "-"} +
+
+
+
+
+ {playerInfoB.score ?? "-"} +
+
+
+
+
+						{playerInfoA.code}
+					
+
+
+
+ {playerInfoA.submissionResult?.status}( + {playerInfoA.submissionResult?.nextScore}) +
+
+
    + {playerInfoA.submissionResult?.executionResults.map( + (result, idx) => ( +
  1. +
    +
    + {result.status} {result.label} +
    +
    {result.output}
    +
    +
  2. + ), + )} +
-
-
{scoreB}
-
-
-							{codeB}
-						
+
+
+						{playerInfoB.code}
+					
+
+
+
+ {playerInfoB.submissionResult?.status}( + {playerInfoB.submissionResult?.nextScore}) +
+
+
    + {playerInfoB.submissionResult?.executionResults.map( + (result, idx) => ( +
  1. +
    +
    + {result.status} {result.label} +
    +
    {result.output}
    +
    +
  2. + ), + )} +
+
{problem}
); } diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx index ef72cec..8282fb4 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx @@ -1,8 +1,10 @@ type Props = { - timeLeft: number; + leftTimeSeconds: number; }; -export default function GolfWatchAppStarting({ timeLeft }: Props) { +export default function GolfWatchAppStarting({ + leftTimeSeconds: timeLeft, +}: Props) { return (
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx index d58ec19..17ef2b9 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx @@ -1,3 +1,9 @@ export default function GolfWatchAppWaiting() { - return
Waiting...
; + return ( +
+
+

Waiting...

+
+
+ ); } -- cgit v1.2.3-70-g09d2 From 7ca24d875c88bf1d063e156fac8f03507fc38382 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 20:25:27 +0900 Subject: feat: extends watcher message types --- backend/api/generated.go | 162 ++++++++++++++++++++++++++++------- frontend/app/.server/api/schema.d.ts | 33 ++++++- openapi.yaml | 70 +++++++++++++-- 3 files changed, 223 insertions(+), 42 deletions(-) diff --git a/backend/api/generated.go b/backend/api/generated.go index b33747a..f7fd7e4 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -51,7 +51,22 @@ const ( // Defines values for GameWatcherMessageS2CExecResultPayloadStatus. const ( - GameWatcherMessageS2CExecResultPayloadStatusSuccess GameWatcherMessageS2CExecResultPayloadStatus = "success" + GameWatcherMessageS2CExecResultPayloadStatusCompileError GameWatcherMessageS2CExecResultPayloadStatus = "compile_error" + GameWatcherMessageS2CExecResultPayloadStatusInternalError GameWatcherMessageS2CExecResultPayloadStatus = "internal_error" + GameWatcherMessageS2CExecResultPayloadStatusRuntimeError GameWatcherMessageS2CExecResultPayloadStatus = "runtime_error" + GameWatcherMessageS2CExecResultPayloadStatusSuccess GameWatcherMessageS2CExecResultPayloadStatus = "success" + GameWatcherMessageS2CExecResultPayloadStatusTimeout GameWatcherMessageS2CExecResultPayloadStatus = "timeout" + GameWatcherMessageS2CExecResultPayloadStatusWrongAnswer GameWatcherMessageS2CExecResultPayloadStatus = "wrong_answer" +) + +// 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" ) // Error defines model for Error. @@ -192,11 +207,11 @@ type GameWatcherMessageS2CExecResult struct { // GameWatcherMessageS2CExecResultPayload defines model for GameWatcherMessageS2CExecResultPayload. type GameWatcherMessageS2CExecResultPayload struct { - PlayerID int `json:"player_id"` - Score nullable.Nullable[int] `json:"score"` - Status GameWatcherMessageS2CExecResultPayloadStatus `json:"status"` - Stderr string `json:"stderr"` - Stdout string `json:"stdout"` + PlayerID int `json:"player_id"` + Status GameWatcherMessageS2CExecResultPayloadStatus `json:"status"` + Stderr string `json:"stderr"` + Stdout string `json:"stdout"` + TestcaseID nullable.Nullable[int] `json:"testcase_id"` } // GameWatcherMessageS2CExecResultPayloadStatus defines model for GameWatcherMessageS2CExecResultPayload.Status. @@ -213,6 +228,33 @@ type GameWatcherMessageS2CStartPayload struct { StartAt int `json:"start_at"` } +// GameWatcherMessageS2CSubmit defines model for GameWatcherMessageS2CSubmit. +type GameWatcherMessageS2CSubmit struct { + Data GameWatcherMessageS2CSubmitPayload `json:"data"` + Type string `json:"type"` +} + +// GameWatcherMessageS2CSubmitPayload defines model for GameWatcherMessageS2CSubmitPayload. +type GameWatcherMessageS2CSubmitPayload struct { + PlayerID int `json:"player_id"` + PreliminaryScore int `json:"preliminary_score"` +} + +// GameWatcherMessageS2CSubmitResult defines model for GameWatcherMessageS2CSubmitResult. +type GameWatcherMessageS2CSubmitResult struct { + Data GameWatcherMessageS2CSubmitResultPayload `json:"data"` + Type string `json:"type"` +} + +// GameWatcherMessageS2CSubmitResultPayload defines model for GameWatcherMessageS2CSubmitResultPayload. +type GameWatcherMessageS2CSubmitResultPayload struct { + PlayerID int `json:"player_id"` + Status GameWatcherMessageS2CSubmitResultPayloadStatus `json:"status"` +} + +// GameWatcherMessageS2CSubmitResultPayloadStatus defines model for GameWatcherMessageS2CSubmitResultPayload.Status. +type GameWatcherMessageS2CSubmitResultPayloadStatus string + // Problem defines model for Problem. type Problem struct { Description string `json:"description"` @@ -630,6 +672,32 @@ func (t *GameWatcherMessageS2C) MergeGameWatcherMessageS2CCode(v GameWatcherMess return err } +// AsGameWatcherMessageS2CSubmit returns the union data inside the GameWatcherMessageS2C as a GameWatcherMessageS2CSubmit +func (t GameWatcherMessageS2C) AsGameWatcherMessageS2CSubmit() (GameWatcherMessageS2CSubmit, error) { + var body GameWatcherMessageS2CSubmit + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromGameWatcherMessageS2CSubmit overwrites any union data inside the GameWatcherMessageS2C as the provided GameWatcherMessageS2CSubmit +func (t *GameWatcherMessageS2C) FromGameWatcherMessageS2CSubmit(v GameWatcherMessageS2CSubmit) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeGameWatcherMessageS2CSubmit performs a merge with any union data inside the GameWatcherMessageS2C, using the provided GameWatcherMessageS2CSubmit +func (t *GameWatcherMessageS2C) MergeGameWatcherMessageS2CSubmit(v GameWatcherMessageS2CSubmit) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + // AsGameWatcherMessageS2CExecResult returns the union data inside the GameWatcherMessageS2C as a GameWatcherMessageS2CExecResult func (t GameWatcherMessageS2C) AsGameWatcherMessageS2CExecResult() (GameWatcherMessageS2CExecResult, error) { var body GameWatcherMessageS2CExecResult @@ -656,6 +724,32 @@ func (t *GameWatcherMessageS2C) MergeGameWatcherMessageS2CExecResult(v GameWatch return err } +// AsGameWatcherMessageS2CSubmitResult returns the union data inside the GameWatcherMessageS2C as a GameWatcherMessageS2CSubmitResult +func (t GameWatcherMessageS2C) AsGameWatcherMessageS2CSubmitResult() (GameWatcherMessageS2CSubmitResult, error) { + var body GameWatcherMessageS2CSubmitResult + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromGameWatcherMessageS2CSubmitResult overwrites any union data inside the GameWatcherMessageS2C as the provided GameWatcherMessageS2CSubmitResult +func (t *GameWatcherMessageS2C) FromGameWatcherMessageS2CSubmitResult(v GameWatcherMessageS2CSubmitResult) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeGameWatcherMessageS2CSubmitResult performs a merge with any union data inside the GameWatcherMessageS2C, using the provided GameWatcherMessageS2CSubmitResult +func (t *GameWatcherMessageS2C) MergeGameWatcherMessageS2CSubmitResult(v GameWatcherMessageS2CSubmitResult) 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 GameWatcherMessageS2C) MarshalJSON() ([]byte, error) { b, err := t.union.MarshalJSON() return b, err @@ -1115,33 +1209,35 @@ 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/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 779f11e..719babb 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -190,7 +190,7 @@ export interface components { code: string; }; GameWatcherMessage: components["schemas"]["GameWatcherMessageS2C"]; - GameWatcherMessageS2C: components["schemas"]["GameWatcherMessageS2CStart"] | components["schemas"]["GameWatcherMessageS2CCode"] | components["schemas"]["GameWatcherMessageS2CExecResult"]; + GameWatcherMessageS2C: components["schemas"]["GameWatcherMessageS2CStart"] | components["schemas"]["GameWatcherMessageS2CCode"] | components["schemas"]["GameWatcherMessageS2CSubmit"] | components["schemas"]["GameWatcherMessageS2CExecResult"] | components["schemas"]["GameWatcherMessageS2CSubmitResult"]; GameWatcherMessageS2CStart: { /** @constant */ type: "watcher:s2c:start"; @@ -211,6 +211,17 @@ export interface components { /** @example print('Hello, world!') */ code: string; }; + GameWatcherMessageS2CSubmit: { + /** @constant */ + type: "watcher:s2c:submit"; + data: components["schemas"]["GameWatcherMessageS2CSubmitPayload"]; + }; + GameWatcherMessageS2CSubmitPayload: { + /** @example 1 */ + player_id: number; + /** @example 100 */ + preliminary_score: number; + }; GameWatcherMessageS2CExecResult: { /** @constant */ type: "watcher:s2c:execresult"; @@ -219,18 +230,32 @@ export interface components { GameWatcherMessageS2CExecResultPayload: { /** @example 1 */ player_id: number; + /** @example 1 */ + testcase_id: number | null; /** * @example success * @enum {string} */ - status: "success"; - /** @example 100 */ - score: number | null; + status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; /** @example Hello, world! */ stdout: string; /** @example */ stderr: string; }; + GameWatcherMessageS2CSubmitResult: { + /** @constant */ + type: "watcher:s2c:submitresult"; + data: components["schemas"]["GameWatcherMessageS2CSubmitResultPayload"]; + }; + GameWatcherMessageS2CSubmitResultPayload: { + /** @example 1 */ + player_id: number; + /** + * @example success + * @enum {string} + */ + status: "success" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; + }; }; responses: { /** @description Bad request */ diff --git a/openapi.yaml b/openapi.yaml index 6d9dea3..54f9f3c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -406,7 +406,9 @@ components: oneOf: - $ref: '#/components/schemas/GameWatcherMessageS2CStart' - $ref: '#/components/schemas/GameWatcherMessageS2CCode' + - $ref: '#/components/schemas/GameWatcherMessageS2CSubmit' - $ref: '#/components/schemas/GameWatcherMessageS2CExecResult' + - $ref: '#/components/schemas/GameWatcherMessageS2CSubmitResult' GameWatcherMessageS2CStart: type: object properties: @@ -449,6 +451,29 @@ components: required: - player_id - code + GameWatcherMessageS2CSubmit: + type: object + properties: + type: + type: string + const: "watcher:s2c:submit" + data: + $ref: '#/components/schemas/GameWatcherMessageS2CSubmitPayload' + required: + - type + - data + GameWatcherMessageS2CSubmitPayload: + type: object + properties: + player_id: + type: integer + example: 1 + preliminary_score: + type: integer + example: 100 + required: + - player_id + - preliminary_score GameWatcherMessageS2CExecResult: type: object properties: @@ -466,15 +491,20 @@ components: player_id: type: integer example: 1 + testcase_id: + type: integer + nullable: true + example: 1 status: type: string example: "success" enum: - success - score: - type: integer - nullable: true - example: 100 + - wrong_answer + - timeout + - runtime_error + - internal_error + - compile_error stdout: type: string example: "Hello, world!" @@ -483,9 +513,39 @@ components: example: "" required: - player_id + - testcase_id - status - - score - stdout - stderr + GameWatcherMessageS2CSubmitResult: + type: object + properties: + type: + type: string + const: "watcher:s2c:submitresult" + data: + $ref: '#/components/schemas/GameWatcherMessageS2CSubmitResultPayload' + required: + - type + - data + GameWatcherMessageS2CSubmitResultPayload: + type: object + properties: + player_id: + type: integer + example: 1 + status: + type: string + example: "success" + enum: + - success + - wrong_answer + - timeout + - runtime_error + - internal_error + - compile_error + required: + - player_id + - status # GameWatcherMessageC2S: # oneOf: -- cgit v1.2.3-70-g09d2 From ec03322292d1063ee113a4ad08cfd823cce87850 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 14:16:09 +0900 Subject: chore: add `make all` and `make reset` for local dev --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index bf3b34c..b9320bd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,10 @@ DOCKER_COMPOSE := docker compose -f compose.local.yaml +all: down build reset up + +reset: + echo "UPDATE games SET state = 'waiting_entries', started_at = NULL WHERE game_id = 1;" | make psql-query + .PHONY: build build: ${DOCKER_COMPOSE} build -- cgit v1.2.3-70-g09d2 From b4ab693aa438f3f1a335369568aabe7849fc1370 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 20:25:59 +0900 Subject: feat: implement watch page --- backend/game/hub.go | 153 +++++++++++++++++---- backend/game/message.go | 14 +- frontend/app/components/GolfWatchApp.client.tsx | 88 ++++++++++-- .../GolfWatchApps/GolfWatchAppGaming.tsx | 82 ++++++++--- frontend/app/root.tsx | 2 +- 5 files changed, 283 insertions(+), 56 deletions(-) diff --git a/backend/game/hub.go b/backend/game/hub.go index aa1b9f2..54c559c 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -10,6 +10,7 @@ import ( "time" "github.com/jackc/pgx/v5/pgtype" + "github.com/oapi-codegen/nullable" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/api" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" @@ -118,14 +119,12 @@ func (hub *gameHub) run() { }, } } - for watcher := range hub.watchers { - watcher.s2cMessages <- &watcherMessageS2CStart{ - Type: watcherMessageTypeS2CStart, - Data: watcherMessageS2CStartPayload{ - StartAt: int(startAt.Unix()), - }, - } - } + hub.broadcastToWatchers(&watcherMessageS2CStart{ + Type: watcherMessageTypeS2CStart, + Data: watcherMessageS2CStartPayload{ + StartAt: int(startAt.Unix()), + }, + }) err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{ GameID: int32(hub.game.gameID), StartedAt: pgtype.Timestamp{ @@ -151,15 +150,13 @@ func (hub *gameHub) run() { // TODO: assert game state is gaming log.Printf("code: %v", message.message) code := msg.Data.Code - for watcher := range hub.watchers { - watcher.s2cMessages <- &watcherMessageS2CCode{ - Type: watcherMessageTypeS2CCode, - Data: watcherMessageS2CCodePayload{ - PlayerID: message.client.playerID, - Code: code, - }, - } - } + hub.broadcastToWatchers(&watcherMessageS2CCode{ + Type: watcherMessageTypeS2CCode, + Data: watcherMessageS2CCodePayload{ + PlayerID: message.client.playerID, + Code: code, + }, + }) case *playerMessageC2SSubmit: // TODO: assert game state is gaming log.Printf("submit: %v", message.message) @@ -176,6 +173,13 @@ func (hub *gameHub) run() { // TODO: notify failure to player log.Fatalf("failed to enqueue task: %v", err) } + hub.broadcastToWatchers(&watcherMessageS2CSubmit{ + Type: watcherMessageTypeS2CSubmit, + Data: watcherMessageS2CSubmitPayload{ + PlayerID: message.client.playerID, + PreliminaryScore: codeSize, + }, + }) default: log.Printf("unexpected message type: %T", message.message) } @@ -209,6 +213,12 @@ func (hub *gameHub) run() { } } +func (hub *gameHub) broadcastToWatchers(msg watcherMessageS2C) { + for watcher := range hub.watchers { + watcher.s2cMessages <- msg + } +} + type codeSubmissionError struct { Status string Stdout string @@ -237,7 +247,13 @@ func (hub *gameHub) processTaskResults() { }, } } - // TODO: broadcast to watchers + hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ + Type: watcherMessageTypeS2CSubmitResult, + Data: watcherMessageS2CSubmitResultPayload{ + PlayerID: taskResult.TaskPayload.UserID(), + Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(err.Status), + }, + }) } case *taskqueue.TaskResultCompileSwiftToWasm: err := hub.processTaskResultCompileSwiftToWasm(taskResult) @@ -254,7 +270,22 @@ func (hub *gameHub) processTaskResults() { }, } } - // TODO: broadcast to watchers + 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), + }, + }) } case *taskqueue.TaskResultCompileWasmToNativeExecutable: err := hub.processTaskResultCompileWasmToNativeExecutable(taskResult) @@ -271,11 +302,38 @@ func (hub *gameHub) processTaskResults() { }, } } - // TODO: broadcast to watchers + 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), + }, + }) + } 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: "", + }, + }) } case *taskqueue.TaskResultRunTestcase: + // FIXME: error handling var err error - err = hub.processTaskResultRunTestcase(taskResult) + err1 := hub.processTaskResultRunTestcase(taskResult) _ = err // TODO: handle err? aggregatedStatus, err := hub.q.AggregateTestcaseResults(hub.ctx, int32(taskResult.TaskPayload.SubmissionID)) _ = err // TODO: handle err? @@ -298,7 +356,24 @@ func (hub *gameHub) processTaskResults() { }, } } - // TODO: broadcast to watchers + 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"), + }, + }) continue } for player := range hub.players { @@ -312,7 +387,39 @@ func (hub *gameHub) processTaskResults() { Status: api.GamePlayerMessageS2CExecResultPayloadStatus(aggregatedStatus), }, } - // TODO: broadcast to watchers + } + 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, + }, + }) + } 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: "", + }, + }) + } + if aggregatedStatus != "running" { + hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ + Type: watcherMessageTypeS2CSubmitResult, + Data: watcherMessageS2CSubmitResultPayload{ + PlayerID: taskResult.TaskPayload.UserID(), + Status: api.GameWatcherMessageS2CSubmitResultPayloadStatus(aggregatedStatus), + }, + }) } default: panic("unexpected task result type") diff --git a/backend/game/message.go b/backend/game/message.go index 031222d..1fb30cb 100644 --- a/backend/game/message.go +++ b/backend/game/message.go @@ -77,9 +77,11 @@ func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error } const ( - watcherMessageTypeS2CStart = "watcher:s2c:start" - watcherMessageTypeS2CExecResult = "watcher:s2c:execresult" - watcherMessageTypeS2CCode = "watcher:s2c:code" + watcherMessageTypeS2CStart = "watcher:s2c:start" + watcherMessageTypeS2CCode = "watcher:s2c:code" + watcherMessageTypeS2CSubmit = "watcher:s2c:submit" + watcherMessageTypeS2CExecResult = "watcher:s2c:execresult" + watcherMessageTypeS2CSubmitResult = "watcher:s2c:submitresult" ) type watcherMessageS2C = interface{} @@ -87,3 +89,9 @@ type watcherMessageS2CStart = api.GameWatcherMessageS2CStart type watcherMessageS2CStartPayload = api.GameWatcherMessageS2CStartPayload type watcherMessageS2CCode = api.GameWatcherMessageS2CCode type watcherMessageS2CCodePayload = api.GameWatcherMessageS2CCodePayload +type watcherMessageS2CSubmit = api.GameWatcherMessageS2CSubmit +type watcherMessageS2CSubmitPayload = api.GameWatcherMessageS2CSubmitPayload +type watcherMessageS2CExecResult = api.GameWatcherMessageS2CExecResult +type watcherMessageS2CExecResultPayload = api.GameWatcherMessageS2CExecResultPayload +type watcherMessageS2CSubmitResult = api.GameWatcherMessageS2CSubmitResult +type watcherMessageS2CSubmitResultPayload = api.GameWatcherMessageS2CSubmitResultPayload diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index 355f7e3..a9c9989 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -116,29 +116,91 @@ export default function GolfWatchApp({ } } else if (lastJsonMessage.type === "watcher:s2c:code") { const { player_id, code } = lastJsonMessage.data; - if (player_id === playerA?.user_id) { - setPlayerInfoA((prev) => ({ ...prev, code })); - } else if (player_id === playerB?.user_id) { - setPlayerInfoB((prev) => ({ ...prev, code })); - } else { - throw new Error("Unknown player_id"); - } + const setter = + 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 setter = + player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; + setter((prev) => ({ + ...prev, + submissionResult: { + status: "running", + preliminaryScore: preliminary_score, + verificationResults: game.verification_steps.map((v) => ({ + testcase_id: v.testcase_id, + status: "running", + label: v.label, + stdout: "", + stderr: "", + })), + }, + })); } else if (lastJsonMessage.type === "watcher:s2c:execresult") { - // const { score } = lastJsonMessage.data; - // if (score !== null && (scoreA === null || score < scoreA)) { - // setScoreA(score); - // } + const { player_id, testcase_id, status, stdout, stderr } = + 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, + verificationResults: ret.submissionResult.verificationResults.map( + (v) => + v.testcase_id === testcase_id && v.status === "running" + ? { + ...v, + status, + stdout, + stderr, + } + : v, + ), + }; + return ret; + }); + } else if (lastJsonMessage.type === "watcher:s2c:submitresult") { + const { player_id, status } = 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, + status, + }; + if (status === "success") { + if ( + ret.score === null || + ret.submissionResult.preliminaryScore < ret.score + ) { + ret.score = ret.submissionResult.preliminaryScore; + } + } else { + ret.submissionResult.verificationResults = + ret.submissionResult.verificationResults.map((v) => + v.status === "running" ? { ...v, status: "canceled" } : v, + ); + } + return ret; + }); } } else { setGameState("waiting"); } } }, [ + game.verification_steps, lastJsonMessage, readyState, gameState, - playerInfoA, - playerInfoB, playerA?.user_id, playerB?.user_id, ]); diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 470a00c..992ce7a 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -14,17 +14,57 @@ export type PlayerInfo = { }; type SubmissionResult = { - status: string; - nextScore: number; - executionResults: ExecutionResult[]; + status: + | "running" + | "success" + | "wrong_answer" + | "timeout" + | "compile_error" + | "runtime_error" + | "internal_error"; + preliminaryScore: number; + verificationResults: VerificationResult[]; }; -type ExecutionResult = { - status: string; +type VerificationResult = { + testcase_id: number | null; + status: + | "running" + | "success" + | "wrong_answer" + | "timeout" + | "compile_error" + | "runtime_error" + | "internal_error" + | "canceled"; label: string; - output: string; + stdout: string; + stderr: 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, playerInfoA, @@ -40,7 +80,7 @@ export default function GolfWatchAppGaming({ const scoreA = playerInfoA.score ?? 0; const scoreB = playerInfoB.score ?? 0; const totalScore = scoreA + scoreB; - return totalScore === 0 ? 50 : (scoreA / totalScore) * 100; + return totalScore === 0 ? 50 : (scoreB / totalScore) * 100; })(); return ( @@ -68,7 +108,7 @@ export default function GolfWatchAppGaming({ {playerInfoB.score ?? "-"}
-
+
 						{playerInfoA.code}
@@ -76,19 +116,24 @@ export default function GolfWatchAppGaming({
 				
- {playerInfoA.submissionResult?.status}( - {playerInfoA.submissionResult?.nextScore}) + {submissionResultStatusToLabel( + playerInfoA.submissionResult?.status ?? null, + )}{" "} + ({playerInfoA.submissionResult?.preliminaryScore})
    - {playerInfoA.submissionResult?.executionResults.map( + {playerInfoA.submissionResult?.verificationResults.map( (result, idx) => (
  1. {result.status} {result.label}
    -
    {result.output}
    +
    + {result.stdout} + {result.stderr} +
  2. ), @@ -103,19 +148,24 @@ export default function GolfWatchAppGaming({
- {playerInfoB.submissionResult?.status}( - {playerInfoB.submissionResult?.nextScore}) + {submissionResultStatusToLabel( + playerInfoB.submissionResult?.status ?? null, + )}{" "} + ({playerInfoB.submissionResult?.preliminaryScore ?? "-"})
    - {playerInfoB.submissionResult?.executionResults.map( + {playerInfoB.submissionResult?.verificationResults.map( (result, idx) => (
  1. {result.status} {result.label}
    -
    {result.output}
    +
    + {result.stdout} + {result.stderr} +
  2. ), diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 054474a..4d7a661 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -21,7 +21,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - + {children} -- cgit v1.2.3-70-g09d2 From 04ff82d35e9cbd3d2a86204260f58a370fda88da Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 10 Aug 2024 21:58:13 +0900 Subject: feat(frontend): show status indicator icon --- .../app/components/ExecStatusIndicatorIcon.tsx | 45 +++++++++++++++++++ .../GolfWatchApps/GolfWatchAppGaming.tsx | 8 +++- frontend/app/routes/_index.tsx | 4 ++ frontend/package-lock.json | 50 ++++++++++++++++++++-- frontend/package.json | 3 ++ 5 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 frontend/app/components/ExecStatusIndicatorIcon.tsx diff --git a/frontend/app/components/ExecStatusIndicatorIcon.tsx b/frontend/app/components/ExecStatusIndicatorIcon.tsx new file mode 100644 index 0000000..a76e957 --- /dev/null +++ b/frontend/app/components/ExecStatusIndicatorIcon.tsx @@ -0,0 +1,45 @@ +import { + faBan, + faCircleCheck, + faCircleExclamation, + faRotate, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +type Props = { + status: string; +}; + +export default function ExecStatusIndicatorIcon({ status }: Props) { + switch (status) { + case "running": + return ( + + ); + case "success": + return ( + + ); + case "canceled": + return ( + + ); + default: + return ( + + ); + } +} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 992ce7a..53d5bce 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,3 +1,5 @@ +import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon"; + type Props = { problem: string; playerInfoA: PlayerInfo; @@ -128,7 +130,8 @@ export default function GolfWatchAppGaming({
  3. - {result.status} {result.label} + {" "} + {result.label}
    {result.stdout} @@ -160,7 +163,8 @@ export default function GolfWatchAppGaming({
  4. - {result.status} {result.label} + {" "} + {result.label}
    {result.stdout} diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index 2fcf1f2..25b9c81 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -1,7 +1,11 @@ +import { config } from "@fortawesome/fontawesome-svg-core"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link } from "@remix-run/react"; +import "@fortawesome/fontawesome-svg-core/styles.css"; import { ensureUserNotLoggedIn } from "../.server/auth"; +config.autoAddCss = false; + export const meta: MetaFunction = () => [ { title: "iOSDC Japan 2024 Albatross.swift" }, ]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d5c12fe..b706788 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,6 +6,9 @@ "": { "name": "iosdc-japan-2024-albatross-frontend", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "@remix-run/node": "^2.10.3", "@remix-run/react": "^2.10.3", "@remix-run/serve": "^2.10.3", @@ -1306,6 +1309,48 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -8087,7 +8132,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8953,7 +8997,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9102,8 +9145,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-refresh": { "version": "0.14.2", diff --git a/frontend/package.json b/frontend/package.json index 44af089..4c630a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,9 @@ "start": "remix-serve ./build/server/index.js" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "@remix-run/node": "^2.10.3", "@remix-run/react": "^2.10.3", "@remix-run/serve": "^2.10.3", -- cgit v1.2.3-70-g09d2