aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/.server/api/schema.d.ts42
-rw-r--r--frontend/app/components/GolfPlayApp.tsx1
-rw-r--r--frontend/app/components/GolfWatchApp.tsx136
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx3
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx3
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx39
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx7
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx3
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx33
9 files changed, 266 insertions, 1 deletions
diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts
index 40a3347..1d3313e 100644
--- a/frontend/app/.server/api/schema.d.ts
+++ b/frontend/app/.server/api/schema.d.ts
@@ -261,6 +261,48 @@ export interface components {
/** @example print('Hello, world!') */
code: string;
};
+ GameWatcherMessage: components["schemas"]["GameWatcherMessageS2C"];
+ GameWatcherMessageS2C: components["schemas"]["GameWatcherMessageS2CStart"] | components["schemas"]["GameWatcherMessageS2CCode"] | components["schemas"]["GameWatcherMessageS2CExecResult"];
+ GameWatcherMessageS2CStart: {
+ /** @constant */
+ type: "watcher:s2c:start";
+ data: components["schemas"]["GameWatcherMessageS2CStartPayload"];
+ };
+ GameWatcherMessageS2CStartPayload: {
+ /** @example 946684800 */
+ start_at: number;
+ };
+ GameWatcherMessageS2CCode: {
+ /** @constant */
+ type: "watcher:s2c:code";
+ data: components["schemas"]["GameWatcherMessageS2CCodePayload"];
+ };
+ GameWatcherMessageS2CCodePayload: {
+ /** @example 1 */
+ player_id: number;
+ /** @example print('Hello, world!') */
+ code: string;
+ };
+ GameWatcherMessageS2CExecResult: {
+ /** @constant */
+ type: "watcher:s2c:execresult";
+ data: components["schemas"]["GameWatcherMessageS2CExecResultPayload"];
+ };
+ GameWatcherMessageS2CExecResultPayload: {
+ /** @example 1 */
+ player_id: number;
+ /**
+ * @example success
+ * @enum {string}
+ */
+ status: "success";
+ /** @example 100 */
+ score: number | null;
+ /** @example Hello, world! */
+ stdout: string;
+ /** @example */
+ stderr: string;
+ };
};
responses: never;
parameters: never;
diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx
index c6c20d4..13afb22 100644
--- a/frontend/app/components/GolfPlayApp.tsx
+++ b/frontend/app/components/GolfPlayApp.tsx
@@ -66,7 +66,6 @@ export default function GolfPlayApp({ game }: { game: Game }) {
}, [gameState, startedAt, game.duration_seconds]);
const [currentScore, setCurrentScore] = useState<number | null>(null);
- void setCurrentScore;
const onCodeChange = useDebouncedCallback((code: string) => {
console.log("player:c2s:code");
diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx
new file mode 100644
index 0000000..bcd1f0f
--- /dev/null
+++ b/frontend/app/components/GolfWatchApp.tsx
@@ -0,0 +1,136 @@
+import type { components } from "../.server/api/schema";
+import { useState, useEffect } from "react";
+import useWebSocket, { ReadyState } from "react-use-websocket";
+import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting";
+import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting";
+import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting";
+import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming";
+import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished";
+
+type WebSocketMessage = components["schemas"]["GameWatcherMessageS2C"];
+
+type Game = components["schemas"]["Game"];
+type Problem = components["schemas"]["Problem"];
+
+type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
+
+export default function GolfWatchApp({ game }: { game: Game }) {
+ // const socketUrl = `wss://t.nil.ninja/iosdc/2024/sock/golf/${game.game_id}/play`;
+ const socketUrl =
+ process.env.NODE_ENV === "development"
+ ? `ws://localhost:8002/sock/golf/${game.game_id}/play`
+ : `ws://api-server/sock/golf/${game.game_id}/play`;
+
+ const { lastJsonMessage, readyState } = useWebSocket<WebSocketMessage>(
+ socketUrl,
+ {},
+ );
+
+ const [gameState, setGameState] = useState<GameState>("connecting");
+
+ const [problem, setProblem] = useState<Problem | null>(null);
+
+ const [startedAt, setStartedAt] = useState<number | null>(null);
+
+ const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
+
+ useEffect(() => {
+ if (gameState === "starting" && startedAt !== null) {
+ const timer1 = setInterval(() => {
+ setTimeLeftSeconds((prev) => {
+ if (prev === null) {
+ return null;
+ }
+ if (prev <= 1) {
+ clearInterval(timer1);
+ setGameState("gaming");
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ const timer2 = setInterval(() => {
+ const nowSec = Math.floor(Date.now() / 1000);
+ const finishedAt = startedAt + game.duration_seconds;
+ if (nowSec >= finishedAt) {
+ clearInterval(timer2);
+ setGameState("finished");
+ }
+ }, 1000);
+
+ return () => {
+ clearInterval(timer1);
+ clearInterval(timer2);
+ };
+ }
+ }, [gameState, startedAt, game.duration_seconds]);
+
+ const [scoreA, setScoreA] = useState<number | null>(null);
+ const [scoreB, setScoreB] = useState<number | null>(null);
+ const [codeA, setCodeA] = useState<string>("");
+ const [codeB, setCodeB] = useState<string>("");
+
+ if (readyState === ReadyState.UNINSTANTIATED) {
+ throw new Error("WebSocket is not connected");
+ }
+
+ useEffect(() => {
+ if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
+ if (gameState !== "finished") {
+ setGameState("connecting");
+ }
+ } else if (readyState === ReadyState.CONNECTING) {
+ setGameState("connecting");
+ } else if (readyState === ReadyState.OPEN) {
+ if (lastJsonMessage !== null) {
+ console.log(lastJsonMessage.type);
+ if (lastJsonMessage.type === "watcher:s2c:start") {
+ if (
+ gameState !== "starting" &&
+ gameState !== "gaming" &&
+ gameState !== "finished"
+ ) {
+ const { start_at } = lastJsonMessage.data;
+ setStartedAt(start_at);
+ const nowSec = Math.floor(Date.now() / 1000);
+ setTimeLeftSeconds(start_at - nowSec);
+ setGameState("starting");
+ }
+ } else if (lastJsonMessage.type === "watcher:s2c:code") {
+ const { player_id, code } = lastJsonMessage.data;
+ setCodeA(code);
+ } else if (lastJsonMessage.type === "watcher:s2c:execresult") {
+ const { score } = lastJsonMessage.data;
+ if (score !== null && (scoreA === null || score < scoreA)) {
+ setScoreA(score);
+ }
+ }
+ } else {
+ setGameState("waiting");
+ }
+ }
+ }, [lastJsonMessage, readyState, gameState, scoreA]);
+
+ if (gameState === "connecting") {
+ return <GolfWatchAppConnecting />;
+ } else if (gameState === "waiting") {
+ return <GolfWatchAppWaiting />;
+ } else if (gameState === "starting") {
+ return <GolfWatchAppStarting timeLeft={timeLeftSeconds!} />;
+ } else if (gameState === "gaming") {
+ return (
+ <GolfWatchAppGaming
+ problem={problem!.description}
+ codeA={codeA}
+ scoreA={scoreA}
+ codeB={codeB}
+ scoreB={scoreB}
+ />
+ );
+ } else if (gameState === "finished") {
+ return <GolfWatchAppFinished />;
+ } else {
+ return null;
+ }
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
new file mode 100644
index 0000000..07e359f
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
@@ -0,0 +1,3 @@
+export default function GolfWatchAppConnecting() {
+ return <div>Connecting...</div>;
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx
new file mode 100644
index 0000000..330d1a6
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx
@@ -0,0 +1,3 @@
+export default function GolfWatchAppFinished() {
+ return <div>Finished</div>;
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
new file mode 100644
index 0000000..d58a04f
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
@@ -0,0 +1,39 @@
+export default function GolfWatchAppGaming({
+ problem,
+ codeA,
+ scoreA,
+ codeB,
+ scoreB,
+}: {
+ problem: string;
+ codeA: string;
+ scoreA: number | null;
+ codeB: string;
+ scoreB: number | null;
+}) {
+ return (
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <div style={{ display: "flex", flex: 1, justifyContent: "center" }}>
+ <div>{problem}</div>
+ </div>
+ <div style={{ display: "flex", flex: 3 }}>
+ <div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
+ <div style={{ flex: 1, justifyContent: "center" }}>{scoreA}</div>
+ <div style={{ flex: 3 }}>
+ <pre>
+ <code>{codeA}</code>
+ </pre>
+ </div>
+ </div>
+ <div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
+ <div style={{ flex: 1, justifyContent: "center" }}>{scoreB}</div>
+ <div style={{ flex: 3 }}>
+ <pre>
+ <code>{codeB}</code>
+ </pre>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
new file mode 100644
index 0000000..643af93
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
@@ -0,0 +1,7 @@
+export default function GolfWatchAppStarting({
+ timeLeft,
+}: {
+ timeLeft: number;
+}) {
+ return <div>Starting... ({timeLeft} s)</div>;
+}
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
new file mode 100644
index 0000000..6733b3b
--- /dev/null
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
@@ -0,0 +1,3 @@
+export default function GolfWatchAppWaiting() {
+ return <div>Waiting...</div>;
+}
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
new file mode 100644
index 0000000..e1cb5d7
--- /dev/null
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -0,0 +1,33 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { isAuthenticated } from "../.server/auth";
+import { apiClient } from "../.server/api/client";
+import { useLoaderData } from "@remix-run/react";
+import GolfWatchApp from "../components/GolfWatchApp";
+
+export async function loader({ params, request }: LoaderFunctionArgs) {
+ const { token } = await isAuthenticated(request, {
+ failureRedirect: "/login",
+ });
+ const { data, error } = await apiClient.GET("/games/{game_id}", {
+ params: {
+ path: {
+ game_id: Number(params.gameId),
+ },
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+ return {
+ game: data,
+ };
+}
+
+export default function GolfWatch() {
+ const { game } = useLoaderData<typeof loader>();
+
+ return <GolfWatchApp game={game} />;
+}