aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-12 02:11:30 +0900
committernsfisis <nsfisis@gmail.com>2024-08-12 03:57:56 +0900
commit8cbb00ae115545a5803f6d08283985ca089d7e41 (patch)
treeaaea5583102fe6a15274ecbd85adf628b044ec92
parent699c5ce665bae6bcc406a0f7de994bb218a9977e (diff)
downloadphperkaigi-2025-albatross-8cbb00ae115545a5803f6d08283985ca089d7e41.tar.gz
phperkaigi-2025-albatross-8cbb00ae115545a5803f6d08283985ca089d7e41.tar.zst
phperkaigi-2025-albatross-8cbb00ae115545a5803f6d08283985ca089d7e41.zip
feat: add `submitresult` message
-rw-r--r--backend/api/generated.go125
-rw-r--r--backend/api/handler.go3
-rw-r--r--backend/game/hub.go150
-rw-r--r--backend/game/message.go11
-rw-r--r--frontend/app/.server/api/schema.d.ts22
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx82
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx13
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx8
-rw-r--r--openapi/api-server.yaml42
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