diff --git a/client/src/components/game/GamePage.tsx b/client/src/components/game/GamePage.tsx index 0f97a0c..8648cdc 100644 --- a/client/src/components/game/GamePage.tsx +++ b/client/src/components/game/GamePage.tsx @@ -1,20 +1,26 @@ "use client"; // TODO: restructure -import { Chessboard } from "react-chessboard"; import { IconCopy } from "@tabler/icons-react"; -import { useState, useEffect, useContext, useReducer, useRef } from "react"; -import type { KeyboardEvent, FormEvent } from "react"; -//import Image from "next/image"; -import type { Game } from "@chessu/types"; -import type { Message } from "@/types"; -import { io } from "socket.io-client"; -import { API_URL } from "@/config"; +import type { FormEvent, KeyboardEvent } from "react"; + import { SessionContext } from "@/context/session"; +import { useContext, useEffect, useReducer, useRef, useState } from "react"; + +import type { Message } from "@/types"; +import type { Game } from "@chessu/types"; + +import type { Move, Square } from "chess.js"; import { Chess } from "chess.js"; -import type { Square, Move } from "chess.js"; -import { initSocket, lobbyReducer, squareReducer } from "./handlers"; +import { Chessboard } from "react-chessboard"; + +import { API_URL } from "@/config"; +import { io } from "socket.io-client"; + +import { lobbyReducer, squareReducer } from "./reducers"; +import { initSocket } from "./socketEvents"; +import { syncPgn, syncSide } from "./utils"; const socket = io(API_URL, { withCredentials: true, autoConnect: false }); @@ -52,45 +58,10 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { handleResize(); if (lobby.pgn && lobby.actualGame.pgn() !== lobby.pgn) { - lobby.actualGame.loadPgn(lobby.pgn as string); - - const lastMove = lobby.actualGame.history({ verbose: true }).pop(); - - let lastMoveSquares = undefined; - let kingSquare = undefined; - if (lastMove) { - lastMoveSquares = { - [lastMove.from]: { background: "rgba(255, 255, 0, 0.4)" }, - [lastMove.to]: { background: "rgba(255, 255, 0, 0.4)" } - }; - } - if (lobby.actualGame.inCheck()) { - const kingPos = lobby.actualGame.board().reduce((acc, row, index) => { - const squareIndex = row.findIndex( - (square) => square && square.type === "k" && square.color === lobby.actualGame.turn() - ); - return squareIndex >= 0 ? `${String.fromCharCode(squareIndex + 97)}${8 - index}` : acc; - }, ""); - kingSquare = { - [kingPos]: { - background: "radial-gradient(red, rgba(255,0,0,.4), transparent 70%)", - borderRadius: "50%" - } - }; - } - updateCustomSquares({ - lastMove: lastMoveSquares, - check: kingSquare - }); + syncPgn(lobby.pgn, lobby, { updateCustomSquares }); } - if (lobby.black?.id === session?.user?.id) { - if (lobby.side !== "b") updateLobby({ type: "setSide", payload: "b" }); - } else if (lobby.white?.id === session?.user?.id) { - if (lobby.side !== "w") updateLobby({ type: "setSide", payload: "w" }); - } else if (lobby.side !== "s") { - updateLobby({ type: "setSide", payload: "s" }); - } + syncSide(session.user, undefined, lobby, { updateLobby }); initSocket(session.user, socket, lobby, { updateLobby, diff --git a/client/src/components/game/handlers.ts b/client/src/components/game/handlers.ts deleted file mode 100644 index 82632be..0000000 --- a/client/src/components/game/handlers.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Dispatch } from "react"; -import type { Action, Lobby, Message, CustomSquares } from "@/types"; -import { Chess } from "chess.js"; -import type { Game, User } from "@chessu/types"; -import type { Socket } from "socket.io-client"; - -export function lobbyReducer(lobby: Lobby, action: Action): Lobby { - switch (action.type) { - case "updateLobby": - return { ...lobby, ...action.payload }; - - case "setSide": - return { ...lobby, side: action.payload }; - - case "setGame": - return { ...lobby, actualGame: action.payload }; - - default: - throw new Error("Invalid action type"); - } -} - -export function squareReducer(squares: CustomSquares, action: Partial) { - return { ...squares, ...action }; -} - -export function initSocket( - user: User, - socket: Socket, - lobby: Lobby, - actions: { - updateLobby: Dispatch; - addMessage: Function; - updateCustomSquares: Dispatch>; - makeMove: Function; - } -) { - socket.on("connect", () => { - console.log("connected!"); - socket.emit("joinLobby", lobby.code); - }); - socket.on("disconnect", () => { - console.log("disconnected!"); - }); - // TODO: handle disconnect - - socket.on("chat", (message: Message) => { - actions.addMessage(message); - }); - - socket.on("receivedLatestGame", (latestGame: Game) => { - if (latestGame.pgn && latestGame.pgn !== lobby.actualGame.pgn()) { - lobby.actualGame.loadPgn(latestGame.pgn as string); - - const lastMove = lobby.actualGame.history({ verbose: true }).pop(); - - let lastMoveSquares = undefined; - let kingSquare = undefined; - if (lastMove) { - lastMoveSquares = { - [lastMove.from]: { background: "rgba(255, 255, 0, 0.4)" }, - [lastMove.to]: { background: "rgba(255, 255, 0, 0.4)" } - }; - } - if (lobby.actualGame.inCheck()) { - const kingPos = lobby.actualGame.board().reduce((acc, row, index) => { - const squareIndex = row.findIndex( - (square) => - square && - square.type === "k" && - square.color === lobby.actualGame.turn() - ); - return squareIndex >= 0 - ? `${String.fromCharCode(squareIndex + 97)}${8 - index}` - : acc; - }, ""); - kingSquare = { - [kingPos]: { - background: "radial-gradient(red, rgba(255,0,0,.4), transparent 70%)", - borderRadius: "50%" - } - }; - } - actions.updateCustomSquares({ - lastMove: lastMoveSquares, - check: kingSquare - }); - } - actions.updateLobby({ type: "updateLobby", payload: latestGame }); - - if (latestGame.black?.id === user?.id) { - if (lobby.side !== "b") actions.updateLobby({ type: "setSide", payload: "b" }); - } else if (latestGame.white?.id === user?.id) { - if (lobby.side !== "w") actions.updateLobby({ type: "setSide", payload: "w" }); - } else if (lobby.side !== "s") { - actions.updateLobby({ type: "setSide", payload: "s" }); - } - }); - - socket.on("receivedMove", (m: { from: string; to: string; promotion?: string }) => { - const success = actions.makeMove(m); - if (!success) { - socket.emit("getLatestGame"); - } - }); - - socket.on("userJoinedAsPlayer", ({ name, side }: { name: string; side: "white" | "black" }) => { - actions.addMessage({ - author: { name: "server" }, - message: `${name} is now playing as ${side}.` - }); - }); - - socket.on( - "gameOver", - ({ - reason, - winnerName, - winnerSide - }: { - reason: string; - winnerName?: string; - winnerSide?: string; - }) => { - const m = { - author: { name: "server" } - } as Message; - - if (reason === "checkmate") { - m.message = `${winnerName} (${winnerSide}) has won by checkmate.`; - } else { - let message = "The game has ended in a draw"; - if (reason === "repetition") { - message = message.concat(" due to threefold repetition"); - } else if (reason === "insufficient") { - message = message.concat(" due to insufficient material"); - } else if (reason === "stalemate") { - message = "The game has been drawn due to stalemate"; - } - m.message = message.concat("."); - } - actions.addMessage(m); - } - ); -} diff --git a/client/src/components/game/reducers.ts b/client/src/components/game/reducers.ts new file mode 100644 index 0000000..a6410b7 --- /dev/null +++ b/client/src/components/game/reducers.ts @@ -0,0 +1,21 @@ +import type { Action, CustomSquares, Lobby } from "@/types"; + +export function lobbyReducer(lobby: Lobby, action: Action): Lobby { + switch (action.type) { + case "updateLobby": + return { ...lobby, ...action.payload }; + + case "setSide": + return { ...lobby, side: action.payload }; + + case "setGame": + return { ...lobby, actualGame: action.payload }; + + default: + throw new Error("Invalid action type"); + } +} + +export function squareReducer(squares: CustomSquares, action: Partial) { + return { ...squares, ...action }; +} diff --git a/client/src/components/game/socketEvents.ts b/client/src/components/game/socketEvents.ts new file mode 100644 index 0000000..4911116 --- /dev/null +++ b/client/src/components/game/socketEvents.ts @@ -0,0 +1,85 @@ +import type { Action, CustomSquares, Lobby, Message } from "@/types"; +import type { Game, User } from "@chessu/types"; +import type { Dispatch } from "react"; +import type { Socket } from "socket.io-client"; +import { syncPgn, syncSide } from "./utils"; + +export function initSocket( + user: User, + socket: Socket, + lobby: Lobby, + actions: { + updateLobby: Dispatch; + addMessage: Function; + updateCustomSquares: Dispatch>; + makeMove: Function; + } +) { + socket.on("connect", () => { + console.log("connected!"); + socket.emit("joinLobby", lobby.code); + }); + socket.on("disconnect", () => { + console.log("disconnected!"); + }); + // TODO: handle disconnect + + socket.on("chat", (message: Message) => { + actions.addMessage(message); + }); + + socket.on("receivedLatestGame", (latestGame: Game) => { + if (latestGame.pgn && latestGame.pgn !== lobby.actualGame.pgn()) { + syncPgn(latestGame.pgn, lobby, actions); + } + actions.updateLobby({ type: "updateLobby", payload: latestGame }); + + syncSide(user, latestGame, lobby, actions); + }); + + socket.on("receivedMove", (m: { from: string; to: string; promotion?: string }) => { + const success = actions.makeMove(m); + if (!success) { + socket.emit("getLatestGame"); + } + }); + + socket.on("userJoinedAsPlayer", ({ name, side }: { name: string; side: "white" | "black" }) => { + actions.addMessage({ + author: { name: "server" }, + message: `${name} is now playing as ${side}.` + }); + }); + + socket.on( + "gameOver", + ({ + reason, + winnerName, + winnerSide + }: { + reason: string; + winnerName?: string; + winnerSide?: string; + }) => { + const m = { + author: { name: "server" } + } as Message; + + if (reason === "checkmate") { + m.message = `${winnerName} (${winnerSide}) has won by checkmate.`; + } else { + let message = "The game has ended in a draw"; + if (reason === "repetition") { + message = message.concat(" due to threefold repetition"); + } else if (reason === "insufficient") { + message = message.concat(" due to insufficient material"); + } else if (reason === "stalemate") { + message = "The game has been drawn due to stalemate"; + } + m.message = message.concat("."); + } + actions.addMessage(m); + } + ); +} diff --git a/client/src/components/game/utils.ts b/client/src/components/game/utils.ts new file mode 100644 index 0000000..21813ad --- /dev/null +++ b/client/src/components/game/utils.ts @@ -0,0 +1,59 @@ +import type { Action, CustomSquares, Lobby } from "@/types"; +import type { Game, User } from "@chessu/types"; +import type { Dispatch } from "react"; + +export const syncPgn = ( + latestPgn: string, + lobby: Lobby, + actions: { + updateCustomSquares: Dispatch>; + } +) => { + lobby.actualGame.loadPgn(latestPgn as string); + + const lastMove = lobby.actualGame.history({ verbose: true }).pop(); + + let lastMoveSquares = undefined; + let kingSquare = undefined; + if (lastMove) { + lastMoveSquares = { + [lastMove.from]: { background: "rgba(255, 255, 0, 0.4)" }, + [lastMove.to]: { background: "rgba(255, 255, 0, 0.4)" } + }; + } + if (lobby.actualGame.inCheck()) { + const kingPos = lobby.actualGame.board().reduce((acc, row, index) => { + const squareIndex = row.findIndex( + (square) => + square && square.type === "k" && square.color === lobby.actualGame.turn() + ); + return squareIndex >= 0 ? `${String.fromCharCode(squareIndex + 97)}${8 - index}` : acc; + }, ""); + kingSquare = { + [kingPos]: { + background: "radial-gradient(red, rgba(255,0,0,.4), transparent 70%)", + borderRadius: "50%" + } + }; + } + actions.updateCustomSquares({ + lastMove: lastMoveSquares, + check: kingSquare + }); +}; + +export const syncSide = ( + user: User, + game: Game | undefined, + lobby: Lobby, + actions: { updateLobby: Dispatch } +) => { + if (!game) game = lobby; + if (game.black?.id === user?.id) { + if (lobby.side !== "b") actions.updateLobby({ type: "setSide", payload: "b" }); + } else if (game.white?.id === user?.id) { + if (lobby.side !== "w") actions.updateLobby({ type: "setSide", payload: "w" }); + } else if (lobby.side !== "s") { + actions.updateLobby({ type: "setSide", payload: "s" }); + } +};