diff --git a/apps/web/src/routes/_components/main-menu/new-section-human.component.tsx b/apps/web/src/routes/_components/main-menu/new-section-human.component.tsx index f23f3cf..70069df 100644 --- a/apps/web/src/routes/_components/main-menu/new-section-human.component.tsx +++ b/apps/web/src/routes/_components/main-menu/new-section-human.component.tsx @@ -3,11 +3,12 @@ import { FC, useState } from "react"; import { Link, useNavigate } from "react-router"; import { MainMenuSection } from "../../../shared/enum"; -import { useMainMenuStore } from "../../_stores"; +import { useGameStore, useMainMenuStore } from "../../_stores"; export const NewGameHumanSection: FC = () => { const navigate = useNavigate(); const { setSection } = useMainMenuStore(); + const { reset: resetGame } = useGameStore(); const [joinRoom, setJoinRoom] = useState(false); @@ -17,11 +18,14 @@ export const NewGameHumanSection: FC = () => { if (!joinRoom) navigate("/play?mode=human"); const formData = new FormData(event.currentTarget); - const roomId = formData.get("room-id") as keyof SupportedAiModel | null; + const roomId = formData.get("room-id") as + | keyof typeof SupportedAiModel + | null; if (!roomId) return; navigate(`/play?mode=human&roomID=${roomId}`); + resetGame(); }; return ( @@ -65,7 +69,7 @@ export const NewGameHumanSection: FC = () => { diff --git a/apps/web/src/routes/_components/main-menu/new-section-simulation.component.tsx b/apps/web/src/routes/_components/main-menu/new-section-simulation.component.tsx index 2c100c7..6da8683 100644 --- a/apps/web/src/routes/_components/main-menu/new-section-simulation.component.tsx +++ b/apps/web/src/routes/_components/main-menu/new-section-simulation.component.tsx @@ -3,11 +3,12 @@ import { FC } from "react"; import { useNavigate } from "react-router"; import { MainMenuSection } from "../../../shared/enum"; -import { useMainMenuStore } from "../../_stores"; +import { useGameStore, useMainMenuStore } from "../../_stores"; export const NewGameSimulationSection: FC = () => { const navigate = useNavigate(); const { setSection } = useMainMenuStore(); + const { reset } = useGameStore(); const renderSupportedAiModels = () => { return Object.keys(SupportedAiModel) @@ -43,8 +44,12 @@ export const NewGameSimulationSection: FC = () => { event.preventDefault(); const formData = new FormData(event.currentTarget); - const ai1 = formData.get("select-ai-1") as keyof SupportedAiModel | null; - const ai2 = formData.get("select-ai-2") as keyof SupportedAiModel | null; + const ai1 = formData.get("select-ai-1") as + | keyof typeof SupportedAiModel + | null; + const ai2 = formData.get("select-ai-2") as + | keyof typeof SupportedAiModel + | null; if ( !ai1 || @@ -55,6 +60,7 @@ export const NewGameSimulationSection: FC = () => { return; navigate(`/play?mode=simulation&ai1=${ai1}&ai2=${ai2}`); + reset(); }; return ( diff --git a/apps/web/src/routes/_hooks/index.ts b/apps/web/src/routes/_hooks/index.ts deleted file mode 100644 index 8d7e54f..0000000 --- a/apps/web/src/routes/_hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./use-socket.hook"; diff --git a/apps/web/src/routes/_hooks/use-socket.hook.ts b/apps/web/src/routes/_hooks/use-socket.hook.ts deleted file mode 100644 index ed6ea2c..0000000 --- a/apps/web/src/routes/_hooks/use-socket.hook.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { - ColorSide, - DEFAULT_FEN, - GameUpdatedPayload, - PlayerEntity, - SOCKET_JOINED_ROOM_TOKEN, - SOCKET_MOVE_PERFORMED_TOKEN, - SOCKET_ROOM_CREATED_TOKEN, - SocketAuthInterface -} from "@chess-d/shared"; -import { validateFen } from "chess.js"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useSearchParams } from "react-router"; -import { merge } from "rxjs"; -import { io, Socket } from "socket.io-client"; - -import { PlayerModel } from "../../shared/models"; - -/** @internal */ -interface UseSocketReturnType { - socket: Socket; - currentPlayer?: PlayerModel; - opponentPlayer?: PlayerModel; - init: (props?: { roomID?: string; side?: ColorSide; fen?: string }) => void; -} - -/** @internal */ -interface RoomJoinedPayload { - room: { fen: string; players: PlayerEntity[] }; - player: PlayerEntity; - roomID: string; -} - -export const useSocket = (): UseSocketReturnType => { - const [searchParams, setSearchParams] = useSearchParams(); - - const socket = useMemo( - () => - io("http://192.168.1.65:3000", { - autoConnect: false - }), - [] - ); - - const [currentPlayer, setCurrentPlayer] = useState(); - const [opponentPlayer, setOpponentPlayer] = useState< - PlayerModel | undefined - >(); - - const init: UseSocketReturnType["init"] = useCallback( - (props) => { - socket.auth = { - roomID: props?.roomID ?? searchParams.get("roomID"), - side: props?.side ?? ColorSide.black, - fen: props?.fen ?? DEFAULT_FEN - } satisfies SocketAuthInterface; - - socket.connect(); - }, - - [searchParams, socket] - ); - - const onRoomCreated = useCallback( - (data: RoomJoinedPayload) => { - console.log("Room created:", data); - - const player = new PlayerModel(); - player.setIdentity(data.player); - - setCurrentPlayer(player); - setSearchParams((prev) => [...prev, ["roomID", data.roomID]]); - }, - [setSearchParams] - ); - - const onJoinedRoom = useCallback( - (data: RoomJoinedPayload) => { - const player = new PlayerModel(); - player.setIdentity(data.player); - - if (!currentPlayer) { - const [opponentEntity] = data.room.players; - - if (opponentEntity) { - const opponent = new PlayerModel(); - opponent.setIdentity(opponentEntity); - - setOpponentPlayer(opponent); - } - - setCurrentPlayer(player); - } else { - currentPlayer.host = true; - setOpponentPlayer(player); - } - - console.log("Joined room:", data); - }, - [currentPlayer] - ); - - const onDisconnect = useCallback(() => { - console.log("Disconnected from server."); - }, []); - - const onError = useCallback( - (error: Error) => { - console.warn("Socket error:", error); - - if (error.cause === "ROOM_NOT_FOUND") { - setSearchParams((prev) => - [...prev].filter(([key]) => key !== "roomID") - ); - socket.auth = { ...socket.auth, roomID: undefined }; - } - - setTimeout(() => socket.connect(), 1000); - }, - [setSearchParams, socket] - ); - - const onMovePerformed = useCallback( - (data: GameUpdatedPayload) => { - opponentPlayer?.next({ token: "PLACED_PIECE", value: data }); - console.log("Opponent performed move:", data); - }, - [opponentPlayer] - ); - - useEffect(() => { - socket.on(SOCKET_ROOM_CREATED_TOKEN, onRoomCreated); - socket.on(SOCKET_JOINED_ROOM_TOKEN, onJoinedRoom); - socket.on(SOCKET_MOVE_PERFORMED_TOKEN, onMovePerformed); - socket.on("disconnect", onDisconnect); - socket.on("error", onError); - - return () => { - socket.off(SOCKET_ROOM_CREATED_TOKEN, onRoomCreated); - socket.off(SOCKET_JOINED_ROOM_TOKEN, onJoinedRoom); - socket.off(SOCKET_MOVE_PERFORMED_TOKEN, onMovePerformed); - socket.off("disconnect", onDisconnect); - socket.off("error", onError); - }; - }, [ - onDisconnect, - onError, - onJoinedRoom, - onMovePerformed, - onRoomCreated, - socket - ]); - - useEffect(() => { - const players: PlayerModel[] = []; - - if (currentPlayer) players.push(currentPlayer); - // if (opponentPlayer) players.push(opponentPlayer); - - const subscription = merge(...players).subscribe((payload) => { - const { token, value } = payload; - const { fen = "", turn } = value || {}; - const fenValidation = validateFen(fen); - - if ( - token === "NOTIFIED" && - fenValidation.ok && - turn === currentPlayer?.color - ) - socket.emit(SOCKET_MOVE_PERFORMED_TOKEN, value); - }); - - return () => subscription?.unsubscribe(); - }, [currentPlayer, socket]); - - return { - socket, - init, - currentPlayer, - opponentPlayer - }; -}; diff --git a/apps/web/src/routes/play/_components/with-human.component.tsx b/apps/web/src/routes/play/_components/with-human.component.tsx index 0eb9575..e20c3e9 100644 --- a/apps/web/src/routes/play/_components/with-human.component.tsx +++ b/apps/web/src/routes/play/_components/with-human.component.tsx @@ -1,67 +1,212 @@ +import { + ColorSide, + DEFAULT_FEN, + GameUpdatedPayload, + PlayerEntity, + SOCKET_JOINED_ROOM_TOKEN, + SOCKET_MOVE_PERFORMED_TOKEN, + SOCKET_ROOM_CREATED_TOKEN, + SocketAuthInterface +} from "@chess-d/shared"; import { Move } from "chess.js"; -import { FC, useEffect } from "react"; -import { merge, Subscription } from "rxjs"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "react-router"; +import { merge } from "rxjs"; +import { io } from "socket.io-client"; import { PlayerModel } from "../../../shared/models"; -import { EngineGameUpdatedMessageEventPayload } from "../../../shared/types"; -import { useSocket } from "../../_hooks"; - -export interface WithHumanComponentProps { - performPieceMove: (move: Move) => void; - onGameUpdate: ( - callback: ( - payload: EngineGameUpdatedMessageEventPayload["value"] - ) => unknown - ) => void; +import { + GAME_UPDATED_TOKEN, + PIECE_WILL_MOVE_TOKEN +} from "../../../shared/tokens"; +import { + EngineGameUpdatedMessageEventPayload, + MessageEventPayload +} from "../../../shared/types"; +import { useGameStore } from "../../_stores"; + +/** @internal */ +interface RoomJoinedPayload { + room: { fen: string; players: PlayerEntity[] }; + player: PlayerEntity; + roomID: string; } -export const WithHumanComponent: FC = ({ - performPieceMove, - onGameUpdate -}) => { - const { - init: initSocket, - currentPlayer: currentSocketPlayer, - opponentPlayer: opponentSocketPlayer - } = useSocket(); +export interface WithHumanComponentProps {} + +export const WithHumanComponent: FC = () => { + const { app } = useGameStore(); + const socket = useMemo( + () => + io("http://localhost:3000", { + autoConnect: false + }), + [] + ); + + const [searchParams, setSearchParams] = useSearchParams(); + + const [currentPlayer, setCurrentPlayer] = useState(); + const [opponentPlayer, setOpponentPlayer] = useState< + PlayerModel | undefined + >(); + + const onRoomCreated = useCallback( + (data: RoomJoinedPayload) => { + console.log("Room created:", data); + + const player = new PlayerModel(); + player.setEntity(data.player); + + setCurrentPlayer(player); + setSearchParams((prev) => [...prev, ["roomID", data.roomID]]); + }, + [setSearchParams] + ); + + const onJoinedRoom = useCallback( + (data: RoomJoinedPayload) => { + const player = new PlayerModel(); + player.setEntity(data.player); + + if (!currentPlayer) { + const [opponentEntity] = data.room.players; + + if (opponentEntity) { + const opponent = new PlayerModel(); + opponent.setEntity(opponentEntity); + + setOpponentPlayer(opponent); + } + + setCurrentPlayer(player); + } else { + currentPlayer.host = true; + setOpponentPlayer(player); + } + + console.log("Joined room:", data); + }, + [currentPlayer] + ); + + const onDisconnect = useCallback(() => { + console.log("Disconnected from server."); + }, []); + + const onError = useCallback( + (error: Error) => { + console.warn("Socket error:", error); + + if (error.cause === "ROOM_NOT_FOUND") { + setSearchParams((prev) => + [...prev].filter(([key]) => key !== "roomID") + ); + socket.auth = { ...socket.auth, roomID: undefined }; + } + + setTimeout(() => socket.connect(), 1000); + }, + [setSearchParams, socket] + ); + + const onMovePerformed = useCallback( + (data: GameUpdatedPayload) => { + opponentPlayer?.next({ token: "PLACED_PIECE", value: data }); + console.log("Opponent performed move:", data); + }, + [opponentPlayer] + ); + + const moveBoardPiece = useCallback( + (move: Move) => { + app?.worker()?.postMessage?.({ + token: PIECE_WILL_MOVE_TOKEN, + value: move + } satisfies MessageEventPayload); + }, + [app] + ); useEffect(() => { - initSocket(); + socket.on(SOCKET_ROOM_CREATED_TOKEN, onRoomCreated); + socket.on(SOCKET_JOINED_ROOM_TOKEN, onJoinedRoom); + socket.on(SOCKET_MOVE_PERFORMED_TOKEN, onMovePerformed); + socket.on("disconnect", onDisconnect); + socket.on("error", onError); + + socket.auth = { + roomID: searchParams.get("roomID"), + side: ColorSide.black, + fen: DEFAULT_FEN + } satisfies SocketAuthInterface; - return () => {}; - }, [initSocket]); + socket.connect(); + + return () => { + socket.off(SOCKET_ROOM_CREATED_TOKEN, onRoomCreated); + socket.off(SOCKET_JOINED_ROOM_TOKEN, onJoinedRoom); + socket.off(SOCKET_MOVE_PERFORMED_TOKEN, onMovePerformed); + socket.off("disconnect", onDisconnect); + socket.off("error", onError); + }; + }, [ + onDisconnect, + onError, + onJoinedRoom, + onMovePerformed, + onRoomCreated, + searchParams, + socket + ]); useEffect(() => { const players: PlayerModel[] = []; - if (currentSocketPlayer) players.push(currentSocketPlayer); - if (opponentSocketPlayer) players.push(opponentSocketPlayer); + if (currentPlayer) players.push(currentPlayer); + if (opponentPlayer) players.push(opponentPlayer); + + const subscription = merge(...players).subscribe((payload) => { + const { token, value } = payload; + const { turn, fen, move, entity } = value || {}; - const playersSubscription: Subscription = merge(...players).subscribe( - (payload) => { - if (payload.token === "PLACED_PIECE" && payload.value?.move) - return performPieceMove(payload.value?.move); + if ( + token === "NOTIFIED" && + move && + fen && + fen !== move.before && + entity && + entity.color === turn + ) { + console.log("Player notified:", value); + socket.emit(SOCKET_MOVE_PERFORMED_TOKEN, value); } - ); + if (payload.token === "PLACED_PIECE" && payload.value?.move) + return moveBoardPiece(payload.value?.move); + }); - onGameUpdate((payload) => { - console.log("Game updated", payload); + const handleMessages = ( + payload: MessageEvent + ) => { + if (!payload.data?.token) return; - players.forEach((player) => { - player?.next({ - token: "NOTIFIED", - value: payload + console.log("Received message:", payload.data); + if (payload.data.token === GAME_UPDATED_TOKEN && payload.data?.value?.fen) + players.forEach((player) => { + player.next({ + token: "NOTIFIED", + value: { ...payload.data.value, entity: player.getEntity() } + }); }); - }); - }); + }; - return () => playersSubscription.unsubscribe(); - }, [ - currentSocketPlayer, - onGameUpdate, - opponentSocketPlayer, - performPieceMove - ]); + app?.worker()?.addEventListener("message", handleMessages); + + return () => { + subscription.unsubscribe(); + app?.worker?.().removeEventListener("message", handleMessages); + }; + }, [app, currentPlayer, opponentPlayer, moveBoardPiece, socket]); return null; }; diff --git a/apps/web/src/routes/play/index.tsx b/apps/web/src/routes/play/index.tsx index 8157026..486ceed 100644 --- a/apps/web/src/routes/play/index.tsx +++ b/apps/web/src/routes/play/index.tsx @@ -5,7 +5,11 @@ import { useSearchParams } from "react-router"; import { GameMode } from "../../shared/enum"; import { getGameModeFromUrl } from "../../shared/utils"; import { useGameStore } from "../_stores"; -import { FreeModeComponent, WithAIComponent } from "./_components"; +import { + FreeModeComponent, + WithAIComponent, + WithHumanComponent +} from "./_components"; /** @internal */ const workerLocation = new URL( @@ -64,6 +68,8 @@ export const PlayRoute: FC = () => { if (gameMode === GameMode.ai || gameMode === GameMode.simulation) return ; + if (gameMode === GameMode.human) return ; + return ; }, [gameMode]);