From df57e43059a230062d903f55f9af7339828875c3 Mon Sep 17 00:00:00 2001 From: nsfisis 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(socketUrl, {}); + + const [gameState, setGameState] = useState("connecting"); + + const [problem, setProblem] = useState(null); + + const [startedAt, setStartedAt] = useState(null); + + const [timeLeftSeconds, setTimeLeftSeconds] = useState(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(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 ; + } else if (gameState === "waiting") { + return ; + } else if (gameState === "starting") { + return ; + } else if (gameState === "gaming") { + return ( + + ); + } else if (gameState === "finished") { + return ; + } else { + return null; + } +} -- cgit v1.2.3-70-g09d2 From 161d82bee9f9e65680516a9cfd392e0cf297eadf Mon Sep 17 00:00:00 2001 From: nsfisis 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 ; -- cgit v1.2.3-70-g09d2