From 8c7d882dfa67bcef37a4f39be6f1ca57f160b816 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 12 Aug 2024 00:32:06 +0900
Subject: feat(frontend): improve play page styling

---
 .../components/GolfPlayApps/GolfPlayAppGaming.tsx  | 65 +++++++++++++---------
 1 file changed, 39 insertions(+), 26 deletions(-)

(limited to 'frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx')

diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 31927a5..03acf5a 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -1,6 +1,11 @@
+import { Link } from "@remix-run/react";
 import React, { useRef } from "react";
+import SubmitButton from "../../components/SubmitButton";
+import BorderedContainer from "../BorderedContainer";
 
 type Props = {
+	gameDisplayName: string;
+	playerDisplayName: string;
 	problemTitle: string;
 	problemDescription: string;
 	onCodeChange: (code: string) => void;
@@ -10,6 +15,8 @@ type Props = {
 };
 
 export default function GolfPlayAppGaming({
+	gameDisplayName,
+	playerDisplayName,
 	problemTitle,
 	problemDescription,
 	onCodeChange,
@@ -30,34 +37,40 @@ export default function GolfPlayAppGaming({
 	};
 
 	return (
-		<div className="min-h-screen flex">
-			<div className="mx-auto flex min-h-full flex-grow">
-				<div className="flex w-1/2 flex-col justify-between p-4">
-					<div>
-						<div className="mb-2 text-xl font-bold">{problemTitle}</div>
-						<div className="text-gray-700">{problemDescription}</div>
-					</div>
-					<div className="mb-4 mt-auto">
-						<div className="mb-2">
-							<div className="font-semibold text-green-500">
-								Score: {currentScore ?? "-"} ({lastExecStatus ?? "-"})
-							</div>
-						</div>
-						<button
-							onClick={handleSubmitButtonClick}
-							className="focus:shadow-outline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none"
-						>
-							Submit
-						</button>
+		<div className="min-h-screen bg-gray-100 flex flex-col">
+			<div className="text-white bg-iosdc-japan flex flex-row justify-between px-4 py-2">
+				<div className="font-bold">
+					<div className="text-gray-100">{gameDisplayName}</div>
+					<div className="text-2xl">03:21</div>
+				</div>
+				<div>
+					<Link to={"/dashboard"} className="font-bold text-xl">
+						{playerDisplayName}
+					</Link>
+				</div>
+			</div>
+			<div className="grow grid grid-cols-3 divide-x divide-gray-300">
+				<div className="p-4">
+					<div className="mb-2 text-xl font-bold">{problemTitle}</div>
+					<div className="p-2">
+						<BorderedContainer>
+							<div className="text-gray-700">{problemDescription}</div>
+						</BorderedContainer>
 					</div>
 				</div>
-				<div className="w-1/2 p-4 flex">
-					<div className="flex-grow">
-						<textarea
-							ref={textareaRef}
-							onChange={handleTextChange}
-							className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-						></textarea>
+				<div className="p-4">
+					<textarea
+						ref={textareaRef}
+						onChange={handleTextChange}
+						className="resize-none h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 transition duration-300"
+					></textarea>
+				</div>
+				<div className="p-4">
+					<SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton>
+					<div className="mb-2 mt-auto">
+						<div className="font-semibold text-green-500">
+							Score: {currentScore ?? "-"} ({lastExecStatus ?? "-"})
+						</div>
 					</div>
 				</div>
 			</div>
-- 
cgit v1.2.3-70-g09d2


From 8cbb00ae115545a5803f6d08283985ca089d7e41 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 12 Aug 2024 02:11:30 +0900
Subject: feat: add `submitresult` message

---
 backend/api/generated.go                           | 125 ++++++++++++-----
 backend/api/handler.go                             |   3 +-
 backend/game/hub.go                                | 150 +++++++++++++--------
 backend/game/message.go                            |  11 +-
 frontend/app/.server/api/schema.d.ts               |  22 ++-
 frontend/app/components/GolfPlayApp.client.tsx     |  82 +++++++++--
 .../components/GolfPlayApps/GolfPlayAppGaming.tsx  |  13 +-
 frontend/app/components/GolfWatchApp.client.tsx    |   8 +-
 openapi/api-server.yaml                            |  42 +++++-
 9 files changed, 328 insertions(+), 128 deletions(-)

(limited to 'frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx')

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
-- 
cgit v1.2.3-70-g09d2


From c6629900f3a4965ceca0f42e90648937bf51dfb5 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 12 Aug 2024 03:56:13 +0900
Subject: feat(frontend): improve play page styling

---
 .../components/GolfPlayApps/GolfPlayAppGaming.tsx  | 50 +++++++++++++++++++---
 frontend/app/components/SubmitStatusLabel.tsx      | 16 +++----
 worker/exec.go                                     | 11 +++--
 worker/models.go                                   |  1 +
 4 files changed, 59 insertions(+), 19 deletions(-)

(limited to 'frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx')

diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 08490a6..667ada2 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -3,6 +3,10 @@ import React, { useRef } from "react";
 import SubmitButton from "../../components/SubmitButton";
 import type { PlayerInfo } from "../../models/PlayerInfo";
 import BorderedContainer from "../BorderedContainer";
+import SubmitStatusLabel from "../SubmitStatusLabel";
+import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon";
+import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 type Props = {
 	gameDisplayName: string;
@@ -40,10 +44,11 @@ export default function GolfPlayAppGaming({
 					<div className="text-gray-100">{gameDisplayName}</div>
 					<div className="text-2xl">03:21</div>
 				</div>
-				<div>
-					<Link to={"/dashboard"} className="font-bold text-xl">
+				<div className="font-bold text-end">
+					<Link to={"/dashboard"} className="text-gray-100">
 						{playerInfo.displayName}
 					</Link>
+					<div className="text-2xl">{playerInfo.score}</div>
 				</div>
 			</div>
 			<div className="grow grid grid-cols-3 divide-x divide-gray-300">
@@ -62,13 +67,44 @@ export default function GolfPlayAppGaming({
 						className="resize-none h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 transition duration-300"
 					></textarea>
 				</div>
-				<div className="p-4">
-					<SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton>
-					<div className="mb-2 mt-auto">
-						<div className="font-semibold text-green-500">
-							Score: {playerInfo.score ?? "-"}
+				<div className="p-4 flex flex-col gap-4">
+					<div className="flex">
+						<SubmitButton onClick={handleSubmitButtonClick}>提出</SubmitButton>
+						<div className="grow font-bold text-xl text-center m-1">
+							<SubmitStatusLabel status={playerInfo.submitResult.status} />
 						</div>
 					</div>
+					<ul className="flex flex-col gap-2">
+						{playerInfo.submitResult.execResults.map((r, idx) => (
+							<li key={r.testcase_id ?? -1} className="flex gap-2">
+								<div className="flex flex-col gap-2 p-2">
+									<div className="w-6">
+										<ExecStatusIndicatorIcon status={r.status} />
+									</div>
+									{idx !== playerInfo.submitResult.execResults.length - 1 && (
+										<div>
+											<FontAwesomeIcon
+												icon={faArrowDown}
+												fixedWidth
+												className="text-gray-500"
+											/>
+										</div>
+									)}
+								</div>
+								<div className="grow p-2 overflow-x-scroll">
+									<BorderedContainer>
+										<div className="font-semibold">{r.label}</div>
+										<div>
+											<code>
+												{r.stdout}
+												{r.stderr}
+											</code>
+										</div>
+									</BorderedContainer>
+								</div>
+							</li>
+						))}
+					</ul>
 				</div>
 			</div>
 		</div>
diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx
index 2d4890d..e0ecc27 100644
--- a/frontend/app/components/SubmitStatusLabel.tsx
+++ b/frontend/app/components/SubmitStatusLabel.tsx
@@ -7,20 +7,20 @@ type Props = {
 export default function SubmitStatusLabel({ status }: Props) {
 	switch (status) {
 		case "waiting_submission":
-			return <span>-</span>;
+			return null;
 		case "running":
-			return <span>Running...</span>;
+			return "実行中...";
 		case "success":
-			return <span>Accepted</span>;
+			return "成功";
 		case "wrong_answer":
-			return <span>Wrong Answer</span>;
+			return "テスト失敗";
 		case "timeout":
-			return <span>Time Limit Exceeded</span>;
+			return "時間切れ";
 		case "compile_error":
-			return <span>Compile Error</span>;
+			return "コンパイルエラー";
 		case "runtime_error":
-			return <span>Runtime Error</span>;
+			return "実行時エラー";
 		case "internal_error":
-			return <span>Internal Error</span>;
+			return "！内部エラー！";
 	}
 }
diff --git a/worker/exec.go b/worker/exec.go
index 9e937f7..37f542b 100644
--- a/worker/exec.go
+++ b/worker/exec.go
@@ -67,11 +67,14 @@ func execCommandWithTimeout(
 	}
 }
 
-func convertCommandErrorToResultType(err error) string {
+func convertCommandErrorToResultType(err error, isCompile bool) string {
 	if err != nil {
 		if err == context.DeadlineExceeded {
 			return resultTimeout
 		}
+		if isCompile {
+			return resultCompileError
+		}
 		return resultRuntimeError
 	}
 	return resultSuccess
@@ -109,7 +112,7 @@ func execSwiftCompile(
 	)
 
 	return swiftCompileResponseData{
-		Status: convertCommandErrorToResultType(err),
+		Status: convertCommandErrorToResultType(err, true),
 		Stdout: stdout,
 		Stderr: stderr,
 	}
@@ -140,7 +143,7 @@ func execWasmCompile(
 	)
 
 	return wasmCompileResponseData{
-		Status: convertCommandErrorToResultType(err),
+		Status: convertCommandErrorToResultType(err, true),
 		Stdout: stdout,
 		Stderr: stderr,
 	}
@@ -170,7 +173,7 @@ func execTestRun(
 	)
 
 	return testRunResponseData{
-		Status: convertCommandErrorToResultType(err),
+		Status: convertCommandErrorToResultType(err, false),
 		Stdout: stdout,
 		Stderr: stderr,
 	}
diff --git a/worker/models.go b/worker/models.go
index 9f60eb0..4a318d0 100644
--- a/worker/models.go
+++ b/worker/models.go
@@ -7,6 +7,7 @@ import (
 
 const (
 	resultSuccess       = "success"
+	resultCompileError  = "compile_error"
 	resultRuntimeError  = "runtime_error"
 	resultTimeout       = "timeout"
 	resultInternalError = "internal_error"
-- 
cgit v1.2.3-70-g09d2


From b37d6f213c2f3b19631e5067f39a7106859faaed Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 12 Aug 2024 05:54:37 +0900
Subject: feat: show left time in play page

---
 backend/admin/handler.go                           | 20 +++----
 backend/api/generated.go                           | 61 +++++++++++-----------
 backend/api/handler.go                             |  8 +--
 backend/game/hub.go                                |  4 +-
 frontend/app/components/GolfPlayApp.client.tsx     | 11 ++--
 .../components/GolfPlayApps/GolfPlayAppGaming.tsx  | 19 +++++--
 frontend/app/components/GolfWatchApp.client.tsx    | 14 ++---
 .../GolfWatchApps/GolfWatchAppGaming.tsx           | 16 +++---
 openapi/api-server.yaml                            |  3 ++
 9 files changed, 90 insertions(+), 66 deletions(-)

(limited to 'frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx')

diff --git a/backend/admin/handler.go b/backend/admin/handler.go
index 5398107..41eacd4 100644
--- a/backend/admin/handler.go
+++ b/backend/admin/handler.go
@@ -229,16 +229,6 @@ func (h *Handler) postGameEdit(c echo.Context) error {
 		}
 	}
 
-	{
-		// TODO:
-		if state != row.State && state == "starting" {
-			err := h.hubs.StartGame(int(gameID))
-			if err != nil {
-				return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
-			}
-		}
-	}
-
 	err = h.q.UpdateGame(c.Request().Context(), db.UpdateGameParams{
 		GameID:          int32(gameID),
 		GameType:        gameType,
@@ -252,5 +242,15 @@ func (h *Handler) postGameEdit(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 	}
 
+	{
+		// TODO:
+		if state != row.State && state == "starting" {
+			err := h.hubs.StartGame(int(gameID))
+			if err != nil {
+				return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+			}
+		}
+	}
+
 	return c.Redirect(http.StatusSeeOther, c.Request().URL.Path)
 }
diff --git a/backend/api/generated.go b/backend/api/generated.go
index 7991f97..7eb5940 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -97,7 +97,7 @@ type Game struct {
 	GameType        GameGameType `json:"game_type"`
 	Players         []User       `json:"players"`
 	Problem         Problem      `json:"problem"`
-	StartedAt       *int         `json:"started_at,omitempty"`
+	StartedAt       *int64       `json:"started_at,omitempty"`
 	State           GameState    `json:"state"`
 }
 
@@ -169,7 +169,7 @@ type GamePlayerMessageS2CStart struct {
 
 // GamePlayerMessageS2CStartPayload defines model for GamePlayerMessageS2CStartPayload.
 type GamePlayerMessageS2CStartPayload struct {
-	StartAt int `json:"start_at"`
+	StartAt int64 `json:"start_at"`
 }
 
 // GamePlayerMessageS2CSubmitResult defines model for GamePlayerMessageS2CSubmitResult.
@@ -235,7 +235,7 @@ type GameWatcherMessageS2CStart struct {
 
 // GameWatcherMessageS2CStartPayload defines model for GameWatcherMessageS2CStartPayload.
 type GameWatcherMessageS2CStartPayload struct {
-	StartAt int `json:"start_at"`
+	StartAt int64 `json:"start_at"`
 }
 
 // GameWatcherMessageS2CSubmit defines model for GameWatcherMessageS2CSubmit.
@@ -1161,33 +1161,34 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
+	"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",
+	"AA==",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/backend/api/handler.go b/backend/api/handler.go
index eca6006..a0ecd4c 100644
--- a/backend/api/handler.go
+++ b/backend/api/handler.go
@@ -84,9 +84,9 @@ func (h *Handler) GetGames(ctx context.Context, _ GetGamesRequestObject, user *a
 	}
 	games := make([]Game, len(gameRows))
 	for i, row := range gameRows {
-		var startedAt *int
+		var startedAt *int64
 		if row.StartedAt.Valid {
-			startedAtTimestamp := int(row.StartedAt.Time.Unix())
+			startedAtTimestamp := row.StartedAt.Time.Unix()
 			startedAt = &startedAtTimestamp
 		}
 		games[i] = Game{
@@ -123,9 +123,9 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use
 		}
 		return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 	}
-	var startedAt *int
+	var startedAt *int64
 	if row.StartedAt.Valid {
-		startedAtTimestamp := int(row.StartedAt.Time.Unix())
+		startedAtTimestamp := row.StartedAt.Time.Unix()
 		startedAt = &startedAtTimestamp
 	}
 	playerRows, err := h.q.ListGamePlayers(ctx, int32(gameID))
diff --git a/backend/game/hub.go b/backend/game/hub.go
index e08ed4a..7c06e05 100644
--- a/backend/game/hub.go
+++ b/backend/game/hub.go
@@ -509,14 +509,14 @@ func (hub *gameHub) startGame() error {
 		player.s2cMessages <- &playerMessageS2CStart{
 			Type: playerMessageTypeS2CStart,
 			Data: playerMessageS2CStartPayload{
-				StartAt: int(startAt.Unix()),
+				StartAt: startAt.Unix(),
 			},
 		}
 	}
 	hub.broadcastToWatchers(&watcherMessageS2CStart{
 		Type: watcherMessageTypeS2CStart,
 		Data: watcherMessageS2CStartPayload{
-			StartAt: int(startAt.Unix()),
+			StartAt: startAt.Unix(),
 		},
 	})
 	err := hub.q.UpdateGameStartedAt(hub.ctx, db.UpdateGameStartedAtParams{
diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx
index dbc8c1b..d527e07 100644
--- a/frontend/app/components/GolfPlayApp.client.tsx
+++ b/frontend/app/components/GolfPlayApp.client.tsx
@@ -43,16 +43,17 @@ export default function GolfPlayApp({
 	const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null);
 
 	useEffect(() => {
-		if (gameState === "starting" && startedAt !== null) {
+		if (
+			(gameState === "starting" || gameState === "gaming") &&
+			startedAt !== null
+		) {
 			const timer1 = setInterval(() => {
 				setLeftTimeSeconds((prev) => {
 					if (prev === null) {
 						return null;
 					}
 					if (prev <= 1) {
-						clearInterval(timer1);
 						setGameState("gaming");
-						return 0;
 					}
 					return prev - 1;
 				});
@@ -196,7 +197,7 @@ export default function GolfPlayApp({
 						// The game has already started.
 						if (gameState !== "gaming" && gameState !== "finished") {
 							setStartedAt(game.started_at);
-							setLeftTimeSeconds(0);
+							setLeftTimeSeconds(game.started_at - nowSec);
 							setGameState("gaming");
 						}
 					} else {
@@ -234,6 +235,8 @@ export default function GolfPlayApp({
 		return (
 			<GolfPlayAppGaming
 				gameDisplayName={game.display_name}
+				gameDurationSeconds={game.duration_seconds}
+				leftTimeSeconds={leftTimeSeconds!}
 				playerInfo={playerInfo}
 				problemTitle={game.problem.title}
 				problemDescription={game.problem.description}
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index 667ada2..4730583 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -1,15 +1,17 @@
+import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Link } from "@remix-run/react";
 import React, { useRef } from "react";
 import SubmitButton from "../../components/SubmitButton";
 import type { PlayerInfo } from "../../models/PlayerInfo";
 import BorderedContainer from "../BorderedContainer";
-import SubmitStatusLabel from "../SubmitStatusLabel";
 import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon";
-import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import SubmitStatusLabel from "../SubmitStatusLabel";
 
 type Props = {
 	gameDisplayName: string;
+	gameDurationSeconds: number;
+	leftTimeSeconds: number;
 	playerInfo: Omit<PlayerInfo, "code">;
 	problemTitle: string;
 	problemDescription: string;
@@ -19,6 +21,8 @@ type Props = {
 
 export default function GolfPlayAppGaming({
 	gameDisplayName,
+	gameDurationSeconds,
+	leftTimeSeconds,
 	playerInfo,
 	problemTitle,
 	problemDescription,
@@ -37,12 +41,19 @@ export default function GolfPlayAppGaming({
 		}
 	};
 
+	const leftTime = (() => {
+		const k = gameDurationSeconds + leftTimeSeconds;
+		const m = Math.floor(k / 60);
+		const s = k % 60;
+		return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+	})();
+
 	return (
 		<div className="min-h-screen bg-gray-100 flex flex-col">
 			<div className="text-white bg-iosdc-japan flex flex-row justify-between px-4 py-2">
 				<div className="font-bold">
 					<div className="text-gray-100">{gameDisplayName}</div>
-					<div className="text-2xl">03:21</div>
+					<div className="text-2xl">{leftTime}</div>
 				</div>
 				<div className="font-bold text-end">
 					<Link to={"/dashboard"} className="text-gray-100">
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 9d3f752..b2f3b69 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -39,16 +39,17 @@ export default function GolfWatchApp({
 	const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null);
 
 	useEffect(() => {
-		if (gameState === "starting" && startedAt !== null) {
+		if (
+			(gameState === "starting" || gameState === "gaming") &&
+			startedAt !== null
+		) {
 			const timer1 = setInterval(() => {
 				setLeftTimeSeconds((prev) => {
 					if (prev === null) {
 						return null;
 					}
 					if (prev <= 1) {
-						clearInterval(timer1);
 						setGameState("gaming");
-						return 0;
 					}
 					return prev - 1;
 				});
@@ -207,7 +208,7 @@ export default function GolfWatchApp({
 						// The game has already started.
 						if (gameState !== "gaming" && gameState !== "finished") {
 							setStartedAt(game.started_at);
-							setLeftTimeSeconds(0);
+							setLeftTimeSeconds(game.started_at - nowSec);
 							setGameState("gaming");
 						}
 					} else {
@@ -245,10 +246,11 @@ export default function GolfWatchApp({
 	} else if (gameState === "gaming") {
 		return (
 			<GolfWatchAppGaming
-				problem={game.problem!.description}
+				gameDurationSeconds={game.duration_seconds}
+				leftTimeSeconds={leftTimeSeconds!}
 				playerInfoA={playerInfoA}
 				playerInfoB={playerInfoB}
-				leftTimeSeconds={leftTimeSeconds!}
+				problem={game.problem!.description}
 			/>
 		);
 	} else if (gameState === "finished") {
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
index 2a852e0..f9647b3 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
@@ -3,23 +3,27 @@ import ExecStatusIndicatorIcon from "../ExecStatusIndicatorIcon";
 import SubmitStatusLabel from "../SubmitStatusLabel";
 
 type Props = {
-	problem: string;
+	gameDurationSeconds: number;
+	leftTimeSeconds: number;
 	playerInfoA: PlayerInfo;
 	playerInfoB: PlayerInfo;
-	leftTimeSeconds: number;
+	problem: string;
 };
 
 export default function GolfWatchAppGaming({
-	problem,
+	gameDurationSeconds,
+	leftTimeSeconds,
 	playerInfoA,
 	playerInfoB,
-	leftTimeSeconds,
+	problem,
 }: Props) {
 	const leftTime = (() => {
-		const m = Math.floor(leftTimeSeconds / 60);
-		const s = leftTimeSeconds % 60;
+		const k = gameDurationSeconds + leftTimeSeconds;
+		const m = Math.floor(k / 60);
+		const s = k % 60;
 		return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
 	})();
+
 	const scoreRatio = (() => {
 		const scoreA = playerInfoA.score ?? 0;
 		const scoreB = playerInfoB.score ?? 0;
diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml
index 036d8d6..94083d3 100644
--- a/openapi/api-server.yaml
+++ b/openapi/api-server.yaml
@@ -212,6 +212,7 @@ components:
         started_at:
           type: integer
           example: 946684800
+          x-go-type: int64
         problem:
           $ref: '#/components/schemas/Problem'
         players:
@@ -286,6 +287,7 @@ components:
         start_at:
           type: integer
           example: 946684800
+          x-go-type: int64
       required:
         - start_at
     GamePlayerMessageS2CExecResult:
@@ -428,6 +430,7 @@ components:
         start_at:
           type: integer
           example: 946684800
+          x-go-type: int64
       required:
         - start_at
     GameWatcherMessageS2CCode:
-- 
cgit v1.2.3-70-g09d2

