aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-03-21 09:23:01 +0900
committernsfisis <nsfisis@gmail.com>2025-03-21 09:50:56 +0900
commit80ee46c81dda5331f66aa401435447f22ff187cd (patch)
tree10830e1e882808b0ecbebd2164fd805160558ca2
parentd379ce3309e5241359b9849fd0170909a140169c (diff)
downloadiosdc-japan-2025-albatross-80ee46c81dda5331f66aa401435447f22ff187cd.tar.gz
iosdc-japan-2025-albatross-80ee46c81dda5331f66aa401435447f22ff187cd.tar.zst
iosdc-japan-2025-albatross-80ee46c81dda5331f66aa401435447f22ff187cd.zip
feat(frontend): show game result in 1v1 watch
-rw-r--r--backend/api/generated.go55
-rw-r--r--backend/api/handler.go21
-rw-r--r--frontend/app/api/schema.d.ts2
-rw-r--r--frontend/app/components/GolfWatchApp.tsx1
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx23
-rw-r--r--frontend/app/states/watch.ts32
-rw-r--r--openapi/api-server.yaml6
7 files changed, 93 insertions, 47 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go
index 0616d51..a6ba8ba 100644
--- a/backend/api/generated.go
+++ b/backend/api/generated.go
@@ -64,9 +64,10 @@ type GameGameType string
// LatestGameState defines model for LatestGameState.
type LatestGameState struct {
- Code string `json:"code"`
- Score nullable.Nullable[int] `json:"score"`
- Status ExecutionStatus `json:"status"`
+ BestScoreSubmittedAt nullable.Nullable[int64] `json:"best_score_submitted_at"`
+ Code string `json:"code"`
+ Score nullable.Nullable[int] `json:"score"`
+ Status ExecutionStatus `json:"status"`
}
// Problem defines model for Problem.
@@ -1128,30 +1129,30 @@ func (sh *strictHandler) PostLogin(ctx echo.Context) error {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "H4sIAAAAAAAC/+xYXY/TOBf+K5bfV+ImNC0zGrHdq2EXECuEKli0WiEUuclp68Gxg+3Q6aL895XtfDlx",
- "aWZg2B20czFqEp/v5zw+9mecirwQHLhWePkZF0SSHDRI+7QDkoFMSKl3QtK/iKaCm/eU42X9EUeYkxzw",
- "El96qyIs4WNJJWR4qWUJEVbpDnJixPWhMAJKS8q3uKoiXBC9S7Ykh4RmrQHzslPffJ2gmHINW5C4Mqol",
- "qEJwBTagJyR7DR9LUNo8pYJr4PYnKQpGU+t6fKVclJ3e/0vY4CX+X9wlK3ZfVfxUSlGbykClkhYuS8YW",
- "krWxKsLPhFzTLAN+95Y7U1WEXwn9TJQ8u3uzr4RGG2uqivBb3qAGvoNpz5r5XEsYhU7IYFuKAqSmDgo5",
- "KEW2YH7CNckLZpDzgn8ijHZ1iwJY7eD3rlXyvl0o1leQ2oI/vYa0NP690USX1ibwMjdiXHAwQC45N1oj",
- "rMo0BaVwhPdS8G1CuNrb3tI0B1Fqt9g8JGDDiSzIJSesfvE+6oXRqRu4H+HntpuGycioKhg5JLz+2qky",
- "69EipCkrpa1hoiAVPFOe3NnFPBq1Y4R7Ld4uXRxd6F53aVt8Mo7kJdPUeAuDqN3nkZ9UJUW5ZjT1rDrq",
- "qBevhWBAbLvkhPLEabcRUQ25OoXLt8p5XasjUpKDeS6kWDPIT4mv6mUGt5pIDVlCtOftT+cXF4/PH8/H",
- "SY3w9cOteNi9vTgfobSjzi6t/bxEfv0Dpe1CGWQoBPyXRIPSBjgG+QG0pSIboAzSnUAPdsCYQHshWfbg",
- "51ApVSqkL7kwKeElY2Q9qmoPTqptwS/Sy6Bjh3m0fjdetEpDKVh1hR80Wp+2+hn4fUcVogoR1GV6FH/9",
- "aVIHKfsxuXWuNdVsIFlHFaKDQaZ6jjaafMr2/Qul8DXhHyjfPuVaHsZ5rPt/Yl8ewU0gaeU6p/qbN2Dt",
- "bQ86fTuh6K3nN6Dp38SOo18FBAkwFTyxw5QnEtOcbEHFV2LHZ1fF9gh3kiynPlY3hKkgdzKyBuYbUZps",
- "Nvhoi3aWSgVyhOtHZ6EimaXjDJgwTuKysdJTMiK/NuYmoHF9jFrKN8LOnK5P8CVbEy2FUqjZmdEe1uhy",
- "9QJH+BNI5SaV+Wwxm5soRAGcFBQv8dlsPptjNwTbMseGpu2vLVgYGgxYQn6RmS0ZLLlaVu6N6+/CvdAt",
- "iYPjfPV+MCM/ms9vNLD5EG1dn7Rx2mlktHEGti91pAr+GPiSKo3EBjmJKsLn88UxF9qYY394NEJnp4V6",
- "M7bljTwnhqecC7X9KqpLGX+ut+DqVFG/UU2jk3LeiesOMDCt8oFKTyr0pU3xd6uwkTg/LdEetXxIPAeN",
- "SO1wABKxIZ+42acLoQLoWAk3UK0YOfzippB/DCb2ZPREZIevQMgtp5LQPBaGi39HUIXh7WPqjTs0bUrG",
- "DqgsMqIbsPzrEWbggfQOELOjN7KJOY41typRzXT+JUIyiHMDvRvmfwR+agP/EkENTzFD8DklU8jqT1HK",
- "pjImIuRE7w959ZAFzREJSVAl02gjJKrH2+OAc+PuNHp749b+R3B3S3DtCeSe4NDBwkKxvhwLwm1PdLrz",
- "CO7kHP2HEelRnPpxOM7+IllGjQhhK2/FjchvjMEAG04b0i8Z6zNKx4jKjO7mS04oR80N0z2a8W4Y2HH8",
- "Snf3MQm59T3JDwHaXtyTjo/eHdGpY2SjfApGV7ZGqBG5l1t1MYjBwI2JrbvMOb4Pv7RLvtU+WBCl9kL6",
- "9zrt28WjM3zkLugrLnh4c46uTX/FBnrLqLX4AIP73WvzN+v9PxmKUzIFrd7GbhAIXBtXb725e7B6q0Ai",
- "B5yqqqq/AwAA///orTnDMh4AAA==",
+ "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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 2cb04c5..04dbd5d 100644
--- a/backend/api/handler.go
+++ b/backend/api/handler.go
@@ -186,9 +186,10 @@ func (h *Handler) GetGamePlayLatestState(ctx context.Context, request GetGamePla
if errors.Is(err, pgx.ErrNoRows) {
return GetGamePlayLatestState200JSONResponse{
State: LatestGameState{
- Code: "",
- Score: nullable.NewNullNullable[int](),
- Status: None,
+ Code: "",
+ Score: nullable.NewNullNullable[int](),
+ BestScoreSubmittedAt: nullable.NewNullNullable[int64](),
+ Status: None,
},
}, nil
}
@@ -196,9 +197,10 @@ func (h *Handler) GetGamePlayLatestState(ctx context.Context, request GetGamePla
}
return GetGamePlayLatestState200JSONResponse{
State: LatestGameState{
- Code: row.Code,
- Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }),
- Status: ExecutionStatus(row.Status),
+ Code: row.Code,
+ Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }),
+ BestScoreSubmittedAt: nullable.NewNullableWithValue(row.CreatedAt.Time.Unix()),
+ Status: ExecutionStatus(row.Status),
},
}, nil
}
@@ -222,9 +224,10 @@ func (h *Handler) GetGameWatchLatestStates(ctx context.Context, request GetGameW
status = None
}
states[strconv.Itoa(int(row.UserID))] = LatestGameState{
- Code: code,
- Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }),
- Status: status,
+ Code: code,
+ Score: toNullableWith(row.CodeSize, func(x int32) int { return int(x) }),
+ BestScoreSubmittedAt: nullable.NewNullableWithValue(row.CreatedAt.Time.Unix()),
+ Status: status,
}
if int(row.UserID) == user.UserID && !user.IsAdmin {
diff --git a/frontend/app/api/schema.d.ts b/frontend/app/api/schema.d.ts
index 0cace21..6f69292 100644
--- a/frontend/app/api/schema.d.ts
+++ b/frontend/app/api/schema.d.ts
@@ -201,6 +201,8 @@ export interface components {
code: string;
/** @example 100 */
score: number | null;
+ /** @example 946684800 */
+ best_score_submitted_at: number | null;
status: components["schemas"]["ExecutionStatus"];
};
RankingEntry: {
diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx
index cfd5e74..492d555 100644
--- a/frontend/app/components/GolfWatchApp.tsx
+++ b/frontend/app/components/GolfWatchApp.tsx
@@ -138,7 +138,6 @@ export default function GolfWatchApp({
problemTitle={game.problem.title}
problemDescription={game.problem.description}
sampleCode={game.problem.sample_code}
- gameResult={null /* TODO */}
/>
) : (
<GolfWatchAppGamingMultiplayer
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
index 63ad5f3..168f5d9 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx
@@ -1,6 +1,8 @@
import { useAtomValue } from "jotai";
import {
calcCodeSize,
+ checkGameResultKind,
+ gameStateKindAtom,
gamingLeftTimeSecondsAtom,
latestGameStatesAtom,
} from "../../states/watch";
@@ -23,7 +25,6 @@ type Props = {
problemTitle: string;
problemDescription: string;
sampleCode: string;
- gameResult: "winA" | "winB" | "draw" | null;
};
export default function GolfWatchAppGaming1v1({
@@ -33,16 +34,16 @@ export default function GolfWatchAppGaming1v1({
problemTitle,
problemDescription,
sampleCode,
- gameResult,
}: Props) {
+ const gameStateKind = useAtomValue(gameStateKindAtom);
const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!;
const latestGameStates = useAtomValue(latestGameStatesAtom);
- const stateA = latestGameStates[`${playerProfileA.id}`];
+ const stateA = latestGameStates[`${playerProfileA.id}`] ?? null;
const codeA = stateA?.code ?? "";
const scoreA = stateA?.score ?? null;
const statusA = stateA?.status ?? "none";
- const stateB = latestGameStates[`${playerProfileB.id}`];
+ const stateB = latestGameStates[`${playerProfileB.id}`] ?? null;
const codeB = stateB?.code ?? "";
const scoreB = stateB?.score ?? null;
const statusB = stateB?.status ?? "none";
@@ -50,10 +51,12 @@ export default function GolfWatchAppGaming1v1({
const codeSizeA = calcCodeSize(codeA);
const codeSizeB = calcCodeSize(codeB);
- const topBg = gameResult
- ? gameResult === "winA"
+ const gameResultKind = checkGameResultKind(gameStateKind, stateA, stateB);
+
+ const topBg = gameResultKind
+ ? gameResultKind === "winA"
? "bg-orange-400"
- : gameResult === "winB"
+ : gameResultKind === "winB"
? "bg-purple-400"
: "bg-sky-600"
: "bg-sky-600";
@@ -76,11 +79,11 @@ export default function GolfWatchAppGaming1v1({
</div>
<div className="font-bold text-center">
<div className="text-gray-100">{gameDisplayName}</div>
- {gameResult ? (
+ {gameResultKind ? (
<div className="text-3xl">
- {gameResult === "winA"
+ {gameResultKind === "winA"
? `勝者 ${playerProfileA.displayName}`
- : gameResult === "winB"
+ : gameResultKind === "winB"
? `勝者 ${playerProfileB.displayName}`
: "引き分け"}
</div>
diff --git a/frontend/app/states/watch.ts b/frontend/app/states/watch.ts
index 8c7faa7..2c255f4 100644
--- a/frontend/app/states/watch.ts
+++ b/frontend/app/states/watch.ts
@@ -93,3 +93,35 @@ export function calcCodeSize(code: string): number {
const utf8Encoded = new TextEncoder().encode(trimmed);
return utf8Encoded.length;
}
+
+export type GameResultKind = "winA" | "winB" | "draw";
+
+export function checkGameResultKind(
+ gameStateKind: GameStateKind,
+ stateA: LatestGameState | null,
+ stateB: LatestGameState | null,
+): GameResultKind | null {
+ if (gameStateKind !== "finished") {
+ return null;
+ }
+
+ const scoreA = stateA?.score;
+ const scoreB = stateB?.score;
+ if (scoreA == null && scoreB == null) {
+ return "draw";
+ }
+ if (scoreA == null) {
+ return "winB";
+ }
+ if (scoreB == null) {
+ return "winA";
+ }
+ if (scoreA === scoreB) {
+ // If score is non-null, state and best_score_submitted_at should also be non-null.
+ const submittedAtA = stateA!.best_score_submitted_at!;
+ const submittedAtB = stateB!.best_score_submitted_at!;
+ return submittedAtA < submittedAtB ? "winA" : "winB";
+ } else {
+ return scoreA < scoreB ? "winA" : "winB";
+ }
+}
diff --git a/openapi/api-server.yaml b/openapi/api-server.yaml
index e6842fd..e9915bf 100644
--- a/openapi/api-server.yaml
+++ b/openapi/api-server.yaml
@@ -378,11 +378,17 @@ components:
type: integer
nullable: true
example: 100
+ best_score_submitted_at:
+ type: integer
+ nullable: true
+ example: 946684800
+ x-go-type: int64
status:
$ref: '#/components/schemas/ExecutionStatus'
required:
- code
- score
+ - best_score_submitted_at
- status
RankingEntry:
type: object