aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-10 20:25:59 +0900
committernsfisis <nsfisis@gmail.com>2024-08-10 22:55:06 +0900
commitb4ab693aa438f3f1a335369568aabe7849fc1370 (patch)
tree50b2c7cc3716f3f7f33dec16751a00361d317458
parentec03322292d1063ee113a4ad08cfd823cce87850 (diff)
downloadiosdc-japan-2024-albatross-b4ab693aa438f3f1a335369568aabe7849fc1370.tar.gz
iosdc-japan-2024-albatross-b4ab693aa438f3f1a335369568aabe7849fc1370.tar.zst
iosdc-japan-2024-albatross-b4ab693aa438f3f1a335369568aabe7849fc1370.zip
feat: implement watch page
-rw-r--r--backend/game/hub.go153
-rw-r--r--backend/game/message.go14
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx88
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx82
-rw-r--r--frontend/app/root.tsx2
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 ?? "-"}
</div>
</div>
- <div className="grid grid-cols-[2fr_1fr_2fr_1fr] p-2">
+ <div className="grid grid-cols-[3fr_2fr_3fr_2fr] p-2">
<div>
<pre>
<code>{playerInfoA.code}</code>
@@ -76,19 +116,24 @@ export default function GolfWatchAppGaming({
</div>
<div>
<div>
- {playerInfoA.submissionResult?.status}(
- {playerInfoA.submissionResult?.nextScore})
+ {submissionResultStatusToLabel(
+ playerInfoA.submissionResult?.status ?? null,
+ )}{" "}
+ ({playerInfoA.submissionResult?.preliminaryScore})
</div>
<div>
<ol>
- {playerInfoA.submissionResult?.executionResults.map(
+ {playerInfoA.submissionResult?.verificationResults.map(
(result, idx) => (
<li key={idx}>
<div>
<div>
{result.status} {result.label}
</div>
- <div>{result.output}</div>
+ <div>
+ {result.stdout}
+ {result.stderr}
+ </div>
</div>
</li>
),
@@ -103,19 +148,24 @@ export default function GolfWatchAppGaming({
</div>
<div>
<div>
- {playerInfoB.submissionResult?.status}(
- {playerInfoB.submissionResult?.nextScore})
+ {submissionResultStatusToLabel(
+ playerInfoB.submissionResult?.status ?? null,
+ )}{" "}
+ ({playerInfoB.submissionResult?.preliminaryScore ?? "-"})
</div>
<div>
<ol>
- {playerInfoB.submissionResult?.executionResults.map(
+ {playerInfoB.submissionResult?.verificationResults.map(
(result, idx) => (
<li key={idx}>
<div>
<div>
{result.status} {result.label}
</div>
- <div>{result.output}</div>
+ <div>
+ {result.stdout}
+ {result.stderr}
+ </div>
</div>
</li>
),
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 }) {
<Meta />
<Links />
</head>
- <body>
+ <body className="h-screen">
{children}
<ScrollRestoration />
<Scripts />