From df57e43059a230062d903f55f9af7339828875c3 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 28 Jul 2024 23:28:10 +0900
Subject: feat(frontend): partially implement gaming

---
 frontend/app/components/GolfPlayApp.tsx | 135 ++++++++++++++++++++++++++++++++
 1 file changed, 135 insertions(+)
 create mode 100644 frontend/app/components/GolfPlayApp.tsx

(limited to 'frontend/app/components/GolfPlayApp.tsx')

diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx
new file mode 100644
index 0000000..31d1c44
--- /dev/null
+++ b/frontend/app/components/GolfPlayApp.tsx
@@ -0,0 +1,135 @@
+import type { components } from "../.server/api/schema";
+import { useState, useEffect } from "react";
+import useWebSocket, { ReadyState } from "react-use-websocket";
+import { useDebouncedCallback } from "use-debounce";
+import GolfPlayAppConnecting from "./GolfPlayApps/GolfPlayAppConnecting";
+import GolfPlayAppWaiting from "./GolfPlayApps/GolfPlayAppWaiting";
+import GolfPlayAppStarting from "./GolfPlayApps/GolfPlayAppStarting";
+import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming";
+import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished";
+
+type WebSocketMessage = components["schemas"]["GamePlayerMessageS2C"];
+
+type Game = components["schemas"]["Game"];
+type Problem = components["schemas"]["Problem"];
+
+type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
+
+export default function GolfPlayApp({ 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 { sendJsonMessage, 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 [currentScore, setCurrentScore] = useState<number | null>(null);
+  void setCurrentScore;
+
+  const onCodeChange = useDebouncedCallback((code: string) => {
+    void code;
+    // sendJsonMessage({});
+  }, 1000);
+
+  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 === "player:s2c:prepare") {
+          const { problem } = lastJsonMessage.data;
+          setProblem(problem);
+          console.log("player:c2s:ready");
+          sendJsonMessage({ type: "player:c2s:ready" });
+        } else if (lastJsonMessage.type === "player: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 {
+        setGameState("waiting");
+        console.log("player:c2s:entry");
+        sendJsonMessage({ type: "player:c2s:entry" });
+      }
+    }
+  }, [sendJsonMessage, lastJsonMessage, readyState, gameState]);
+
+  if (gameState === "connecting") {
+    return <GolfPlayAppConnecting />;
+  } else if (gameState === "waiting") {
+    return <GolfPlayAppWaiting />;
+  } else if (gameState === "starting") {
+    return <GolfPlayAppStarting timeLeft={timeLeftSeconds!} />;
+  } else if (gameState === "gaming") {
+    return (
+      <GolfPlayAppGaming
+        problem={problem!.description}
+        onCodeChange={onCodeChange}
+        currentScore={currentScore}
+      />
+    );
+  } else if (gameState === "finished") {
+    return <GolfPlayAppFinished />;
+  } else {
+    return null;
+  }
+}
-- 
cgit v1.2.3-70-g09d2


From 161d82bee9f9e65680516a9cfd392e0cf297eadf Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Mon, 29 Jul 2024 02:58:54 +0900
Subject: feat: handle code and execresult messages

---
 backend/game/hub.go                     | 27 +++++++++++++++++++++++++--
 backend/game/message.go                 | 23 +++++++++++++++++++----
 frontend/app/components/GolfPlayApp.tsx | 17 ++++++++++++++---
 3 files changed, 58 insertions(+), 9 deletions(-)

(limited to 'frontend/app/components/GolfPlayApp.tsx')

diff --git a/backend/game/hub.go b/backend/game/hub.go
index 170f142..c61f2bb 100644
--- a/backend/game/hub.go
+++ b/backend/game/hub.go
@@ -68,7 +68,7 @@ func (hub *gameHub) run() {
 				hub.closeWatcherClient(watcher)
 			}
 		case message := <-hub.playerC2SMessages:
-			switch message.message.(type) {
+			switch msg := message.message.(type) {
 			case *playerMessageC2SEntry:
 				log.Printf("entry: %v", message.message)
 				// TODO: assert state is waiting_entries
@@ -142,11 +142,34 @@ func (hub *gameHub) run() {
 					}
 					hub.game.state = gameStateStarting
 				}
+			case *playerMessageC2SCode:
+				// TODO: assert game state is gaming
+				log.Printf("code: %v", message.message)
+				code := msg.Data.Code
+				score := len(code)
+				message.client.s2cMessages <- &playerMessageS2CExecResult{
+					Type: playerMessageTypeS2CExecResult,
+					Data: playerMessageS2CExecResultPayload{
+						Score:  &score,
+						Status: api.Success,
+					},
+				}
 			default:
 				log.Fatalf("unexpected message type: %T", message.message)
 			}
 		case <-ticker.C:
-			if hub.game.state == gameStateGaming {
+			if hub.game.state == gameStateStarting {
+				if time.Now().After(*hub.game.startedAt) {
+					err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{
+						GameID: int32(hub.game.gameID),
+						State:  string(gameStateGaming),
+					})
+					if err != nil {
+						log.Fatalf("failed to set game state: %v", err)
+					}
+					hub.game.state = gameStateGaming
+				}
+			} else if hub.game.state == gameStateGaming {
 				if time.Now().After(hub.game.startedAt.Add(time.Duration(hub.game.durationSeconds) * time.Second)) {
 					err := hub.q.UpdateGameState(hub.ctx, db.UpdateGameStateParams{
 						GameID: int32(hub.game.gameID),
diff --git a/backend/game/message.go b/backend/game/message.go
index 23774ce..9116bde 100644
--- a/backend/game/message.go
+++ b/backend/game/message.go
@@ -8,10 +8,12 @@ import (
 )
 
 const (
-	playerMessageTypeS2CPrepare = "player:s2c:prepare"
-	playerMessageTypeS2CStart   = "player:s2c:start"
-	playerMessageTypeC2SEntry   = "player:c2s:entry"
-	playerMessageTypeC2SReady   = "player:c2s:ready"
+	playerMessageTypeS2CPrepare    = "player:s2c:prepare"
+	playerMessageTypeS2CStart      = "player:s2c:start"
+	playerMessageTypeS2CExecResult = "player:s2c:execreslut"
+	playerMessageTypeC2SEntry      = "player:c2s:entry"
+	playerMessageTypeC2SReady      = "player:c2s:ready"
+	playerMessageTypeC2SCode       = "player:c2s:code"
 )
 
 type playerMessageC2SWithClient struct {
@@ -26,10 +28,14 @@ type playerMessageS2CPrepare = api.GamePlayerMessageS2CPrepare
 type playerMessageS2CPreparePayload = api.GamePlayerMessageS2CPreparePayload
 type playerMessageS2CStart = api.GamePlayerMessageS2CStart
 type playerMessageS2CStartPayload = api.GamePlayerMessageS2CStartPayload
+type playerMessageS2CExecResult = api.GamePlayerMessageS2CExecResult
+type playerMessageS2CExecResultPayload = api.GamePlayerMessageS2CExecResultPayload
 
 type playerMessageC2S = interface{}
 type playerMessageC2SEntry = api.GamePlayerMessageC2SEntry
 type playerMessageC2SReady = api.GamePlayerMessageC2SReady
+type playerMessageC2SCode = api.GamePlayerMessageC2SCode
+type playerMessageC2SCodePayload = api.GamePlayerMessageC2SCodePayload
 
 func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error) {
 	var typ string
@@ -46,6 +52,15 @@ func asPlayerMessageC2S(raw map[string]json.RawMessage) (playerMessageC2S, error
 		return &playerMessageC2SReady{
 			Type: playerMessageTypeC2SReady,
 		}, nil
+	case playerMessageTypeC2SCode:
+		var payload playerMessageC2SCodePayload
+		if err := json.Unmarshal(raw["data"], &payload); err != nil {
+			return nil, err
+		}
+		return &playerMessageC2SCode{
+			Type: playerMessageTypeC2SCode,
+			Data: payload,
+		}, nil
 	default:
 		return nil, fmt.Errorf("unknown message type: %s", typ)
 	}
diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx
index 31d1c44..c6c20d4 100644
--- a/frontend/app/components/GolfPlayApp.tsx
+++ b/frontend/app/components/GolfPlayApp.tsx
@@ -69,8 +69,11 @@ export default function GolfPlayApp({ game }: { game: Game }) {
   void setCurrentScore;
 
   const onCodeChange = useDebouncedCallback((code: string) => {
-    void code;
-    // sendJsonMessage({});
+    console.log("player:c2s:code");
+    sendJsonMessage({
+      type: "player:c2s:code",
+      data: { code },
+    });
   }, 1000);
 
   if (readyState === ReadyState.UNINSTANTIATED) {
@@ -104,6 +107,14 @@ export default function GolfPlayApp({ game }: { game: Game }) {
             setTimeLeftSeconds(start_at - nowSec);
             setGameState("starting");
           }
+        } else if (lastJsonMessage.type === "player:s2c:execresult") {
+          const { score } = lastJsonMessage.data;
+          if (
+            score !== null &&
+            (currentScore === null || score < currentScore)
+          ) {
+            setCurrentScore(score);
+          }
         }
       } else {
         setGameState("waiting");
@@ -111,7 +122,7 @@ export default function GolfPlayApp({ game }: { game: Game }) {
         sendJsonMessage({ type: "player:c2s:entry" });
       }
     }
-  }, [sendJsonMessage, lastJsonMessage, readyState, gameState]);
+  }, [sendJsonMessage, lastJsonMessage, readyState, gameState, currentScore]);
 
   if (gameState === "connecting") {
     return <GolfPlayAppConnecting />;
-- 
cgit v1.2.3-70-g09d2

