diff options
| -rw-r--r-- | backend/api/generated.go | 125 | ||||
| -rw-r--r-- | backend/api/handler.go | 3 | ||||
| -rw-r--r-- | backend/game/hub.go | 150 | ||||
| -rw-r--r-- | backend/game/message.go | 11 | ||||
| -rw-r--r-- | frontend/app/.server/api/schema.d.ts | 22 | ||||
| -rw-r--r-- | frontend/app/components/GolfPlayApp.client.tsx | 82 | ||||
| -rw-r--r-- | frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx | 13 | ||||
| -rw-r--r-- | frontend/app/components/GolfWatchApp.client.tsx | 8 | ||||
| -rw-r--r-- | openapi/api-server.yaml | 42 |
9 files changed, 328 insertions, 128 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go index f3be897..7991f97 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. @@ -142,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. @@ -160,6 +172,21 @@ type GamePlayerMessageS2CStartPayload struct { StartAt int `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 @@ -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,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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", - "/UzJPYQeNuY8CzBN0/TfAAAA//9NY96x1CQAAA==", + "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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 7ef77e5..eca6006 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -148,7 +148,8 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use } execSteps := make([]ExecStep, len(testcaseIDs)+1) execSteps[0] = ExecStep{ - Label: "Compile", + TestcaseID: nullable.NewNullNullable[int](), + Label: "Compile", } for i, testcaseID := range testcaseIDs { execSteps[i+1] = ExecStep{ diff --git a/backend/game/hub.go b/backend/game/hub.go index 670b05f..490b79e 100644 --- a/backend/game/hub.go +++ b/backend/game/hub.go @@ -137,6 +137,15 @@ func (hub *gameHub) run() { } } +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 @@ -159,18 +168,12 @@ 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.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(err.Status), + }, + }) hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ Type: watcherMessageTypeS2CSubmitResult, Data: watcherMessageS2CSubmitResultPayload{ @@ -182,18 +185,20 @@ func (hub *gameHub) processTaskResults() { 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.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Status: api.GamePlayerMessageS2CExecResultPayloadStatus(err.Status), + Stdout: err.Stdout, + Stderr: err.Stderr, + }, + }) + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(err.Status), + }, + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -214,18 +219,20 @@ func (hub *gameHub) processTaskResults() { 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.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Status: api.GamePlayerMessageS2CExecResultPayloadStatus(err.Status), + Stdout: err.Stdout, + Stderr: err.Stderr, + }, + }) + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(err.Status), + }, + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -243,6 +250,15 @@ func (hub *gameHub) processTaskResults() { }, }) } else { + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + Status: api.GamePlayerMessageS2CExecResultPayloadStatus("success"), + // TODO: inherit the command stdout/stderr. + Stdout: "Successfully compiled", + Stderr: "", + }, + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -268,18 +284,22 @@ 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.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + Status: api.GamePlayerMessageS2CExecResultPayloadStatus("internal_error"), + // TODO: inherit the command stdout/stderr? + Stdout: "", + Stderr: "internal error", + }, + }) + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus("internal_error"), + }, + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -300,19 +320,16 @@ func (hub *gameHub) processTaskResults() { }) continue } - for player := range hub.players { - if player.playerID != taskResult.TaskPayload.UserID() { - continue - } - player.s2cMessages <- &playerMessageS2CExecResult{ + if err1 != nil { + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ Type: playerMessageTypeS2CExecResult, Data: playerMessageS2CExecResultPayload{ - Score: nil, - Status: api.GamePlayerMessageS2CExecResultPayloadStatus(aggregatedStatus), + TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + Status: api.GamePlayerMessageS2CExecResultPayloadStatus(aggregatedStatus), + Stdout: err1.Stdout, + Stderr: err1.Stderr, }, - } - } - if err1 != nil { + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -324,6 +341,16 @@ func (hub *gameHub) processTaskResults() { }, }) } else { + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CExecResult{ + Type: playerMessageTypeS2CExecResult, + Data: playerMessageS2CExecResultPayload{ + TestcaseID: nullable.NewNullableWithValue(int(taskResult.TaskPayload.TestcaseID)), + Status: api.GamePlayerMessageS2CExecResultPayloadStatus("success"), + // TODO: inherit the command stdout/stderr? + Stdout: "Testcase passed", + Stderr: "", + }, + }) hub.broadcastToWatchers(&watcherMessageS2CExecResult{ Type: watcherMessageTypeS2CExecResult, Data: watcherMessageS2CExecResultPayload{ @@ -344,6 +371,13 @@ func (hub *gameHub) processTaskResults() { score = nullable.NewNullableWithValue(int(codeSize)) } } + hub.sendToPlayer(taskResult.TaskPayload.UserID(), &playerMessageS2CSubmitResult{ + Type: playerMessageTypeS2CSubmitResult, + Data: playerMessageS2CSubmitResultPayload{ + Status: api.GamePlayerMessageS2CSubmitResultPayloadStatus(aggregatedStatus), + Score: score, + }, + }) hub.broadcastToWatchers(&watcherMessageS2CSubmitResult{ Type: watcherMessageTypeS2CSubmitResult, Data: watcherMessageS2CSubmitResultPayload{ 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/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts index 5b37081..7fd612e 100644 --- a/frontend/app/.server/api/schema.d.ts +++ b/frontend/app/.server/api/schema.d.ts @@ -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" | "failure" | "timeout" | "internal_error" | "compile_error" | "wrong_answer"; + 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" | "wrong_answer" | "timeout" | "runtime_error" | "internal_error" | "compile_error"; /** @example 100 */ score: number | null; }; diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx index ef3a229..dbc8c1b 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"; @@ -73,9 +74,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"); @@ -94,6 +107,18 @@ export default function GolfPlayApp({ 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) { @@ -123,14 +148,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) { @@ -165,7 +222,6 @@ export default function GolfPlayApp({ lastJsonMessage, readyState, gameState, - currentScore, ]); if (gameState === "connecting") { @@ -178,13 +234,11 @@ export default function GolfPlayApp({ return ( <GolfPlayAppGaming gameDisplayName={game.display_name} - playerDisplayName={player.display_name} + 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 03acf5a..08490a6 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,28 +1,25 @@ 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"; type Props = { gameDisplayName: string; - playerDisplayName: string; + 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, - playerDisplayName, + playerInfo, problemTitle, problemDescription, onCodeChange, onCodeSubmit, - currentScore, - lastExecStatus, }: Props) { const textareaRef = useRef<HTMLTextAreaElement>(null); @@ -45,7 +42,7 @@ export default function GolfPlayAppGaming({ </div> <div> <Link to={"/dashboard"} className="font-bold text-xl"> - {playerDisplayName} + {playerInfo.displayName} </Link> </div> </div> @@ -69,7 +66,7 @@ export default function GolfPlayAppGaming({ <SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton> <div className="mb-2 mt-auto"> <div className="font-semibold text-green-500"> - Score: {currentScore ?? "-"} ({lastExecStatus ?? "-"}) + Score: {playerInfo.score ?? "-"} </div> </div> </div> diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index 7f582c9..9d3f752 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -161,8 +161,8 @@ export default function GolfWatchApp({ setter((prev) => { const ret = { ...prev }; ret.submitResult = { - ...ret.submitResult, - execResults: ret.submitResult.execResults.map((r) => + ...prev.submitResult, + execResults: prev.submitResult.execResults.map((r) => r.testcase_id === testcase_id && r.status === "running" ? { ...r, @@ -182,7 +182,7 @@ export default function GolfWatchApp({ setter((prev) => { const ret = { ...prev }; ret.submitResult = { - ...ret.submitResult, + ...prev.submitResult, status, }; if (status === "success") { @@ -192,7 +192,7 @@ export default function GolfWatchApp({ } } } else { - ret.submitResult.execResults = ret.submitResult.execResults.map( + ret.submitResult.execResults = prev.submitResult.execResults.map( (r) => r.status === "running" ? { ...r, status: "canceled" } : r, ); diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml index d13d738..036d8d6 100644 --- a/openapi/api-server.yaml +++ b/openapi/api-server.yaml @@ -268,6 +268,7 @@ components: oneOf: - $ref: '#/components/schemas/GamePlayerMessageS2CStart' - $ref: '#/components/schemas/GamePlayerMessageS2CExecResult' + - $ref: '#/components/schemas/GamePlayerMessageS2CSubmitResult' GamePlayerMessageS2CStart: type: object properties: @@ -301,16 +302,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 |
