From f9639b5dc491e6d453f388be60212df8bf1c682b Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Wed, 11 Oct 2023 22:43:37 +0530 Subject: [PATCH] added computer mode --- backend/chessbot.js | 16 ++- backend/socket.js | 28 ++++ frontend/src/App.jsx | 17 ++- frontend/src/components/Cell.jsx | 9 +- frontend/src/context/chess-game-context.jsx | 66 +++++---- frontend/src/pages/Chess/ChessBoard.jsx | 32 ++--- frontend/src/pages/Chess/ChessGame.jsx | 27 ++-- frontend/src/pages/Play/ChessGameComputer.jsx | 132 ++++++++++++++++++ frontend/src/pages/Play/Computer.jsx | 24 +++- frontend/src/pages/Play/ComputerGame.jsx | 17 +++ frontend/src/pages/Play/MultiplayerGame.jsx | 17 +++ frontend/src/socket.js | 3 +- 12 files changed, 307 insertions(+), 81 deletions(-) create mode 100644 frontend/src/pages/Play/ChessGameComputer.jsx create mode 100644 frontend/src/pages/Play/ComputerGame.jsx create mode 100644 frontend/src/pages/Play/MultiplayerGame.jsx diff --git a/backend/chessbot.js b/backend/chessbot.js index 2d7b93c..988d181 100644 --- a/backend/chessbot.js +++ b/backend/chessbot.js @@ -6,16 +6,22 @@ const engine = new Engine( "C:\\Users\\MOON\\Downloads\\stockfish-windows-x86-64-avx2\\stockfish\\stockfish-windows-x86-64-avx2.exe" ); -engine.init().then(() => { - nextMove({ position: "r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3" }); -}); +engine + .init() + .then((eng) => { + return eng.setoption("UCI_LimitStrength", true); + }) + .then((eng) => { + eng.setoption("UCI_Elo"); + }); const nextMove = async ({ position }) => { await engine.isready(); console.log("Chess engine ready"); engine.position(position); - const result = await engine.go({ depth: 22 }); + const result = await engine.go({ depth: 10 }); console.log("Best move or position", position, "is", result.bestmove); + return result.bestmove; }; -module.exports = {}; +module.exports = { nextMove }; diff --git a/backend/socket.js b/backend/socket.js index 341bb4c..4c2739f 100644 --- a/backend/socket.js +++ b/backend/socket.js @@ -3,6 +3,7 @@ const { SOCKET_EVENTS } = require("./constants"); const { Chess } = require("chess.js"); const { User } = require("./models/user"); const { Game } = require("./models/game"); +const { nextMove } = require("./chessbot"); const { CHESS_MOVE, CHESS_OPPONENT_MOVE, @@ -61,6 +62,33 @@ function socketIOServerInit(server) { }, }); + const ioBot = io.of("/chessbot"); + + ioBot.on("connection", (socket) => { + let id = socket.id; + console.log(id, "connected on /chessbot"); + const chess = new Chess(); + + socket.onAny((evt) => { + console.log(evt); + }); + + socket.on("disconnect", (reason) => { + console.log(id, "disconnected due to", reason); + }); + + socket.on(CHESS_MOVE, async (roomID, moveData) => { + console.log("CHESS_MOVE"); + chess.move(moveData); + let move = moveData.from + moveData.to; + console.log(move); + const botMove = await nextMove({ position: chess.fen() }); + console.log({ from: botMove.substring(0, 2), to: botMove.substring(2) }); + chess.move({ from: botMove.substring(0, 2), to: botMove.substring(2) }); + socket.emit("CHESS_BOT_MOVE", { from: botMove.substring(0, 2), to: botMove.substring(2) }); + }); + }); + io.on("connection", (socket) => { let id = socket.id; console.log(socket.id, "connected"); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 86566e2..74a3160 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,10 +11,11 @@ import PlayFriend from './pages/Play/PlayFriend' import Play from './pages/Play/Play' import AuthenticationPage from './pages/Authentication/Authentication' import ChallengeFriend, { playFriendAction } from './pages/Play/ChallengeFriend' -import ChessGame from './pages/Chess/ChessGame' import Profile, { action as profileAction } from './pages/Settings/Profile' -import ChessGameContextProvider from './context/chess-game-context' import { getAuthToken, getUserData } from './utils/auth' +import Computer from './pages/Play/Computer' +import ComputerGame from './pages/Play/ComputerGame' +import MultiplayerGame from './pages/Play/MultiplayerGame' const router = createBrowserRouter([{ path: '/', @@ -28,17 +29,15 @@ const router = createBrowserRouter([{ { index: true, element: }, { path: 'friend/:friend_username', element: , action: playFriendAction }, { path: 'friend', element: }, - { path: 'computer', element:
Computer
}, + { path: 'computer', element: }, { path: 'online', element:
Online
} ] }, { - path: "game/friend/:roomID", element: -
- - - -
+ path: "game/friend/:roomID", element: + }, + { + path: "game/computer", element: }, { path: 'settings', element: , children: [ diff --git a/frontend/src/components/Cell.jsx b/frontend/src/components/Cell.jsx index 4960c2b..edaaff3 100644 --- a/frontend/src/components/Cell.jsx +++ b/frontend/src/components/Cell.jsx @@ -9,9 +9,9 @@ import Piece from './Piece'; import { ChessGameContext } from '../context/chess-game-context'; import { SOCKET_EVENTS } from '../constants'; -const { CHESS_MOVE } = SOCKET_EVENTS +const { CHESS_MOVE, GAME_END } = SOCKET_EVENTS -const Cell = ({ cell }) => { +const Cell = ({ cell, callbacks }) => { let roomID = localStorage.getItem('roomID'); let { square, type } = cell; const { getSquareColor, isSquareMarked, handleSquareClick } = useContext(ChessGameContext) @@ -23,9 +23,8 @@ const Cell = ({ cell }) => { let borderWidth = type ? '3px' : '5px' const handleClick = () => { - handleSquareClick(square, (moveData) => { - // moveData contains fen string, from, to squares of the move - socket.emit(CHESS_MOVE, roomID, moveData); + handleSquareClick(square, callbacks.pieceClickCallback, () => { + socket.emit(GAME_END, roomID); }); } diff --git a/frontend/src/context/chess-game-context.jsx b/frontend/src/context/chess-game-context.jsx index 7d9639a..b6b08c4 100644 --- a/frontend/src/context/chess-game-context.jsx +++ b/frontend/src/context/chess-game-context.jsx @@ -2,7 +2,7 @@ import React, { createContext, useReducer, useRef, useState } from 'react' import PropTypes from 'prop-types'; -import { socket } from '../socket'; +import { socket, socketBot } from '../socket'; import { ChessModified, chessInit } from '../utils/chess'; import { DISPATCH_EVENTS, SOCKET_EVENTS } from '../constants'; @@ -25,16 +25,18 @@ const reducer = (state, action) => { let updatedGameHistory = state.gameHistory; let { san, after } = newChessObj.move(action.val); updatedGameHistory.push({ move: san, fen: after }); + let newState; if (newChessObj.isCheckmate()) { - socket.emit(GAME_END, localStorage.getItem('roomID')); - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'CHECKMATE' }; + action.val.callback(); + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'CHECKMATE' }; } else if (newChessObj.isStalemate()) { - socket.emit(GAME_END, localStorage.getItem('roomID')); - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'STALEMATE' }; + action.val.callback(); + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'STALEMATE' }; } else { - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; } + return newState; } case CAPTURE_PIECE: { @@ -42,16 +44,18 @@ const reducer = (state, action) => { let updatedGameHistory = state.gameHistory; let { san, after } = newChessObj.move(action.val); updatedGameHistory.push({ move: san, fen: after }); + let newState; if (newChessObj.isCheckmate()) { - socket.emit(GAME_END, localStorage.getItem('roomID')); - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'CHECKMATE' }; + action.val.callback(); + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'CHECKMATE' }; } else if (newChessObj.isStalemate()) { - socket.emit(GAME_END, localStorage.getItem('roomID')); - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'STALEMATE' }; + action.val.callback(); + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'STALEMATE' }; } else { - return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; + newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; } + return newState; } case JUMP_TO: { @@ -120,21 +124,22 @@ const ChessGameContextProvider = ({ children }) => { const checkAudioRef = useRef(null); // data received through socket - function handleOpponentMove(data) { + function handleOpponentMove(data, callback) { let { from, to } = data; + console.log(data); if (!chessRef.current.get(to)) { - dispatch({ type: MOVE_PIECE, val: { from, to } }); - moveAudioRef.current.play(); + dispatch({ type: MOVE_PIECE, val: { from, to, callback } }); + // moveAudioRef.current.play(); return; } else { - dispatch({ type: CAPTURE_PIECE, val: { from, to } }); - captureAudioRef.current.play(); + dispatch({ type: CAPTURE_PIECE, val: { from, to, callback } }); + // captureAudioRef.current.play(); return; } } // called when user clicks a square - function handleSquareClick(square, emitToSocketCallback) { + function handleSquareClick(square, emitToSocketCallback, callback) { let { type, color } = chessRef.current.get(square); let marked = moveHintsRef.current.includes(square); @@ -143,36 +148,35 @@ const ChessGameContextProvider = ({ children }) => { return dispatch({ type: SELECT_PIECE, val: square }); } if (!type && selectedRef.current && marked) { - dispatch({ type: MOVE_PIECE, val: { from: selected, to: square } }) + dispatch({ type: MOVE_PIECE, val: { from: selected, to: square, callback } }) emitToSocketCallback({ from: selectedRef.current, to: square }) setIsTimerOn(false) - captureAudioRef.current.play(); + moveAudioRef.current.play(); + return; } if (type && marked) { - dispatch({ type: CAPTURE_PIECE, val: { from: selectedRef.current, to: square } }) + console.log({ from: selectedRef.current, to: square }) + dispatch({ type: CAPTURE_PIECE, val: { from: selectedRef.current, to: square, callback } }) emitToSocketCallback({ from: selectedRef.current, to: square }) setIsTimerOn(false); - moveAudioRef.current.play(); + captureAudioRef.current.play(); + return; } - } else { - return; } } - function handleDrop(moveData) { + function handleDrop(moveData, emitToSocketCallback, callback) { let { from, to } = moveData; - // console.log(from, to, ch ess.get(to), chess.ascii()) if (moveHintsRef.current.includes(to)) { if (chessRef.current.get(to)) { - dispatch({ type: CAPTURE_PIECE, val: { from: from, to: to } }); // capture piece + dispatch({ type: CAPTURE_PIECE, val: { from: from, to: to, callback } }); // capture piece captureAudioRef.current.play(); - // setIsTimerOn(false) - socket.emit(CHESS_MOVE, roomID, moveData); + emitToSocketCallback(moveData); } else { - dispatch({ type: MOVE_PIECE, val: { from: from, to: to } }); // move piece + dispatch({ type: MOVE_PIECE, val: { from: from, to: to, callback } }); // move piece moveAudioRef.current.play(); - // setIsTimerOn(false) - socket.emit(CHESS_MOVE, roomID, moveData); + console.log(moveData); + emitToSocketCallback(moveData); } } } diff --git a/frontend/src/pages/Chess/ChessBoard.jsx b/frontend/src/pages/Chess/ChessBoard.jsx index efeb604..a278862 100644 --- a/frontend/src/pages/Chess/ChessBoard.jsx +++ b/frontend/src/pages/Chess/ChessBoard.jsx @@ -7,7 +7,7 @@ import Cell from '../../components/Cell'; import { ChessGameContext } from '../../context/chess-game-context'; import { socket } from '../../socket'; import { SOCKET_EVENTS } from '../../constants'; -const { CHESS_OPPONENT_MOVE, CHESS_MOVE } = SOCKET_EVENTS +const { GAME_END } = SOCKET_EVENTS const useStyles = createStyles((theme) => ({ chessboard: { @@ -35,34 +35,28 @@ const useStyles = createStyles((theme) => ({ } })) -const ChessBoard = () => { +const ChessBoard = ({ callbacks }) => { const { classes } = useStyles(); - const { getChessBoard, handleOpponentMove, handleDrop } = useContext(ChessGameContext) - const roomID = localStorage.getItem('roomID'); + const { getChessBoard, handleDrop } = useContext(ChessGameContext) const chessBoard = getChessBoard(); - const myColor = localStorage.getItem('myColor') - - useEffect(() => { - socket.on(CHESS_OPPONENT_MOVE, handleOpponentMove) - - return () => { - socket.off(CHESS_OPPONENT_MOVE); - } - }, []); + const myColor = localStorage.getItem('myColor'); + const roomID = localStorage.getItem('roomID'); if (myColor === 'w') { return ( { let from = evt.active.id; let to = evt.over.id; - handleDrop({ from, to }); + handleDrop({ from, to }, callbacks.pieceDropCallback, () => { + socket.emit(GAME_END, roomID); + }); }}>
{chessBoard.map((row, rowIndex) => { return ( - {row.map(cell => )} + {row.map(cell => )} ) })} @@ -75,16 +69,16 @@ const ChessBoard = () => { { let from = evt.active.id; let to = evt.over.id; - handleDrop({ from, to }, (moveData) => { - socket.emit(CHESS_MOVE, roomID, moveData); - }) + handleDrop({ from, to }, callbacks.pieceDropCallback, () => { + socket.emit(GAME_END, roomID); + }); }}>
{chessBoard.map((row, rowIndex) => { return ( - {row.map(cell => ).reverse()} + {row.map(cell => ).reverse()} ) }).reverse()} diff --git a/frontend/src/pages/Chess/ChessGame.jsx b/frontend/src/pages/Chess/ChessGame.jsx index 74132f6..19b68fe 100644 --- a/frontend/src/pages/Chess/ChessGame.jsx +++ b/frontend/src/pages/Chess/ChessGame.jsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom' import { useDisclosure } from '@mantine/hooks' import { Avatar, Button, Flex, Image, MediaQuery, Modal, NavLink, Text, Title } from '@mantine/core' -import { socket } from '../../socket' +import { socket, socketBot } from '../../socket' import { getUserData } from '../../utils/auth' import { ChessGameContext } from '../../context/chess-game-context' import ChessBoard from '../Chess/ChessBoard' @@ -27,7 +27,6 @@ const ChessGame = () => { const roomID = localStorage.getItem('roomID'); const navigate = useNavigate(); const opponent = localStorage.getItem('opponent'); - let connected = socket.id; const exitGame = () => { // cleanup game related data @@ -45,6 +44,14 @@ const ChessGame = () => { endGame('RESIGN'); exitGame(); } + const pieceDropCallback = (moveData) => { + socket.emit(CHESS_MOVE, roomID, moveData); + } + + const pieceClickCallback = (moveData) => { + // moveData contains fen string, from, to squares of the move + socket.emit(CHESS_MOVE, roomID, moveData); + } useEffect(() => { socket.connect(); @@ -71,7 +78,11 @@ const ChessGame = () => { console.log('Socket disconnected due to', reason); }); - socket.on(CHESS_OPPONENT_MOVE, handleOpponentMove) + socket.on(CHESS_OPPONENT_MOVE, (data) => { + handleOpponentMove(data, () => { + socket.emit(GAME_END, roomID); + }) + }) socket.on(USER_JOINED_ROOM, () => { setIsWaiting(false); @@ -83,14 +94,10 @@ const ChessGame = () => { return () => { socket.offAny(); - socket.disconnect() + socket.disconnect(); } }, []); - useEffect(() => { - console.log('Connection useEffect()'); - }, [connected]); - if (!hasJoinedRoom) return ( ) @@ -110,7 +117,7 @@ const ChessGame = () => { p="2px" label={isWaiting ? "Waiting for opponent..." : opponent} icon={ - {opponent[0].toUpperCase} + {opponent?.at(0)?.toUpperCase} } description={"description"} /> @@ -128,7 +135,7 @@ const ChessGame = () => { : - + }
{ + const { hasGameEnded, gameEndedReason, handleOpponentMove } = useContext(ChessGameContext); + const [gameEndedModalOpen, modalFunctions] = useDisclosure(true); + const navigate = useNavigate(); + const user = getUserData(); + let username = user.username; + let color = localStorage.getItem('myColor') || 'w'; + const roomID = localStorage.getItem('roomID'); + + + useEffect(() => { + socket.connect(); + + socket.onAny(evt => { + console.log("event", evt); + }) + + socket.on("CHESS_BOT_MOVE", (data) => { + handleOpponentMove(data, () => { + socket.emit(GAME_END, roomID); + }) + }); + + return () => { + socket.offAny(); + socket.disconnect(); + } + }, []); + + const exitGame = () => { + console.log("Ending game"); + socket.disconnect(); + navigate("/play/computer"); + } + + const pieceDropCallback = (moveData) => { + console.log("Hello"); + socket.emit(CHESS_MOVE, roomID, moveData); + } + const pieceClickCallback = (moveData) => { + // moveData contains fen string, from, to squares of the move + socket.emit(CHESS_MOVE, roomID, moveData); + } + + // const resign = () => { + // socket.emit(USER_RESIGNED); + // endGame('RESIGN'); + // exitGame(); + // } + + + return ( + + + Game ended due to {gameEndedReason} + + + + + +
+ + Computer + } + description={"description"} + /> +
+ { + // TODO: handle isWaiting state + false ? + <> + + + + + + + + : + + } +
+ + {username[0].toUpperCase()} + } + description={"description"} + /> +
+
+ + + Game Data + + + + + + + + +
+
+ ) +} + +export default ChessGameComputer \ No newline at end of file diff --git a/frontend/src/pages/Play/Computer.jsx b/frontend/src/pages/Play/Computer.jsx index 53bdd84..e6a5f04 100644 --- a/frontend/src/pages/Play/Computer.jsx +++ b/frontend/src/pages/Play/Computer.jsx @@ -1,8 +1,30 @@ +import { Button, Card, Flex, Image, TextInput, Title } from '@mantine/core' +import { IconSearch } from '@tabler/icons-react' import React from 'react' +import { Link } from 'react-router-dom' const Computer = () => { return ( -
Computer
+ + + + Play with Computer + + + + + + + ) } diff --git a/frontend/src/pages/Play/ComputerGame.jsx b/frontend/src/pages/Play/ComputerGame.jsx new file mode 100644 index 0000000..cb43663 --- /dev/null +++ b/frontend/src/pages/Play/ComputerGame.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import ChessGameContextProvider from '../../context/chess-game-context' +import ChessGameComputer from './ChessGameComputer' + +const ComputerGame = () => { + + + return ( +
+ + + +
+ ) +} + +export default ComputerGame \ No newline at end of file diff --git a/frontend/src/pages/Play/MultiplayerGame.jsx b/frontend/src/pages/Play/MultiplayerGame.jsx new file mode 100644 index 0000000..a5d55ec --- /dev/null +++ b/frontend/src/pages/Play/MultiplayerGame.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import ChessGameContextProvider from '../../context/chess-game-context' +import ChessGame from '../Chess/ChessGame' + +const MultiplayerGame = () => { + + + return ( +
+ + + +
+ ) +} + +export default MultiplayerGame \ No newline at end of file diff --git a/frontend/src/socket.js b/frontend/src/socket.js index c8aaa64..36054ab 100644 --- a/frontend/src/socket.js +++ b/frontend/src/socket.js @@ -1,3 +1,4 @@ import { io } from "socket.io-client"; -const url = import.meta.env.VITE_BACKEND_HOST +const url = import.meta.env.VITE_BACKEND_HOST; export const socket = io(url, { autoConnect: false }); +export const socketBot = io(url + "/chessbot", { autoConnect: false });