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]);