From aa6e463b60878038f68bf0e2f2ce304bb9899179 Mon Sep 17 00:00:00 2001 From: Moon Patel Date: Tue, 4 Jul 2023 16:50:40 +0530 Subject: [PATCH] Game state persistency implemented. Game state is now saved on page refresh. The game state is stored on the server and when the user refreshes the page the game state is fetched from the server on socket re-connection Next step: stop user from making any move when the use is viewing past moves --- backend/package-lock.json | 6 ++++ backend/package.json | 1 + backend/socket.js | 14 ++++++-- frontend/src/components/Cell.jsx | 10 ++++-- frontend/src/constants.js | 1 + frontend/src/context/chess-game-context.jsx | 38 ++++++++++++++++----- frontend/src/pages/Chess/ChessGame.jsx | 5 +-- 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 67d84dc..e88a6f1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", + "chess.js": "^1.0.0-beta.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -144,6 +145,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chess.js": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-1.0.0-beta.6.tgz", + "integrity": "sha512-sqBfX1VL3csSyqVM5ogbKA+aRlZyWDh276ruWXphwI0lDUMs7iYjZs29BOi49f7mXeunJE7cdfnIZhihsyLnsA==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", diff --git a/backend/package.json b/backend/package.json index 10a5959..9c4282f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", + "chess.js": "^1.0.0-beta.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", diff --git a/backend/socket.js b/backend/socket.js index ff9d18d..c67ac5d 100644 --- a/backend/socket.js +++ b/backend/socket.js @@ -2,15 +2,19 @@ const socket = require("socket.io"); const { SOCKET_EVENTS } = require("./constants"); const { CHESS_MOVE, CHESS_OPPONENT_MOVE, CONNECTION, JOIN_ROOM, JOIN_ROOM_ERROR, JOIN_ROOM_SUCCESS, ROOM_FULL } = SOCKET_EVENTS; -// roomID => { timeLimit, players:[{username: {color}}] } +// roomID => { timeLimit,gameHistory , players:[{username: {color}}] } let activeRooms = new Map(); function createRoom(roomID, timeLimit) { console.log(roomID, "created"); - activeRooms.set(roomID, { timeLimit, players: {} }); + activeRooms.set(roomID, { timeLimit, players: {}, gameHistory: [] }); console.log("Currently active rooms", activeRooms.size); } +function getRoom(roomID) { + return activeRooms.get(roomID); +} + // structure of userDetails: {username,color} function addUserToRoom(roomID, userDetails) { console.log(userDetails); @@ -57,7 +61,9 @@ function socketIOServerInit(server) { if (result === JOIN_ROOM_SUCCESS) { socket.join(roomID); io.to(roomID).emit("new user joined the room"); - socket.emit(result); // room joined successfully + console.log(data, "joined"); + let room = getRoom(roomID); + socket.emit(result, room.gameHistory); // room joined successfully } else { socket.emit(result); // room is full } @@ -68,6 +74,8 @@ function socketIOServerInit(server) { socket.on(CHESS_MOVE, (roomID, moveData) => { console.log(moveData); + let room = activeRooms.get(roomID); + room.gameHistory.push(moveData); socket.to(roomID).emit(CHESS_OPPONENT_MOVE, moveData); }); }); diff --git a/frontend/src/components/Cell.jsx b/frontend/src/components/Cell.jsx index 3d436c9..a05cf01 100644 --- a/frontend/src/components/Cell.jsx +++ b/frontend/src/components/Cell.jsx @@ -1,11 +1,14 @@ import React, { useContext, useState } from 'react' import Piece from './Piece'; import { socket } from '../socket'; -import { Box, Flex } from '@mantine/core'; +import { Box, Flex, Modal } from '@mantine/core'; import { useDroppable } from '@dnd-kit/core' import { ChessGameContext } from '../context/chess-game-context'; +import { SOCKET_EVENTS } from '../constants'; +const { CHESS_MOVE } = SOCKET_EVENTS const Cell = ({ cell }) => { + let roomID = localStorage.getItem('roomID'); let { square, type, color } = cell; const { getSquareColor, isSquareMarked, handleSquareClick } = useContext(ChessGameContext) const [isDropped, setIsDropped] = useState(false); @@ -14,7 +17,10 @@ const Cell = ({ cell }) => { let marked = isSquareMarked(square); const handleClick = () => { - handleSquareClick(square); + handleSquareClick(square, (moveData) => { + // moveData contains fen string, from, to squares of the move + socket.emit(CHESS_MOVE, roomID, moveData); + }); } let content = marked ? : ; diff --git a/frontend/src/constants.js b/frontend/src/constants.js index c85cdaf..c9548bc 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -14,4 +14,5 @@ export const DISPATCH_EVENTS = { MOVE_PIECE: "MOVE_PIECE", CAPTURE_PIECE: "CAPTURE_PIECE", JUMP_TO: "JUMP_TO", + SET_GAME_HISTORY: "SET_GAME_HISTORY", }; diff --git a/frontend/src/context/chess-game-context.jsx b/frontend/src/context/chess-game-context.jsx index 94f8f37..1bf9732 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 } from 'react' import { ChessModified, chessInit } from '../../utils/chess'; import { DISPATCH_EVENTS } from '../constants'; import ChessBoard from '../pages/Chess/ChessBoard'; -const { CAPTURE_PIECE, MOVE_PIECE, SELECT_PIECE, JUMP_TO } = DISPATCH_EVENTS +const { CAPTURE_PIECE, MOVE_PIECE, SELECT_PIECE, JUMP_TO, SET_GAME_HISTORY } = DISPATCH_EVENTS export const ChessGameContext = createContext(); // myColor: null, chess: null, chessBoard: null, moveHints: null, selected: null, dispatch: null, handleOpponentMove: null, handleSquareClick: null, getSquareColor: null, isSquareMarked: null, selectPiece: null, handleDrop: null @@ -20,8 +20,8 @@ const reducer = (state, action) => { console.log('Moving', action.val, state.chess.turn()); let newChessObj = new ChessModified({ prop: state.chess.fen(), color: state.chess.myColor }); let updatedGameHistory = state.gameHistory; - let move = newChessObj.move(action.val); - updatedGameHistory.push({ move: move.san, fen: newChessObj.fen() }); + let { san, after } = newChessObj.move(action.val); + updatedGameHistory.push({ move: san, fen: after }); return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(localStorage.getItem('myColor')), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; } case CAPTURE_PIECE: @@ -29,8 +29,8 @@ const reducer = (state, action) => { console.log('Capture', action.val, state.chess.turn()); let newChessObj = new ChessModified({ prop: state.chess.fen(), color: state.chess.myColor }); let updatedGameHistory = state.gameHistory; - let move = newChessObj.move(action.val); - updatedGameHistory.push({ move: move.san, fen: newChessObj.fen() }); + let { san, after } = newChessObj.move(action.val); + updatedGameHistory.push({ move: san, fen: after }); return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(localStorage.getItem('myColor')), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }; } case JUMP_TO: @@ -38,6 +38,17 @@ const reducer = (state, action) => { let index = action.val; return { ...state, currentIndex: index } } + case SET_GAME_HISTORY: + { + let fetchedGameHistory = action.val; + let newChessObj = new ChessModified({ color: state.chess.myColor }); + let updatedGameHistory = []; + for (let i = 0; i < fetchedGameHistory.length; i++) { + let { san, after } = newChessObj.move(fetchedGameHistory[i]); + updatedGameHistory.push({ fen: after, move: san }) + } + return { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(state.chess.myColor), gameHistory: updatedGameHistory } + } default: return state; } @@ -60,7 +71,7 @@ const ChessGameContextProvider = ({ children }) => { let myColor = localStorage.getItem('myColor'); console.log('INSIDE CONTEXT PROVIDER'); const [{ chess, chessBoard, moveHints, selected, gameHistory, currentIndex }, dispatch] = useReducer(reducer, myColor, chessGameStateInit); - console.log(gameHistory) + console.log(gameHistory); console.log(gameHistory); @@ -71,7 +82,7 @@ const ChessGameContextProvider = ({ children }) => { // data received through socket function handleOpponentMove(data) { - let { from, to } = data; + let { from, to, fen } = data; console.log(from + to); if (!chess.get(to)) { console.log('Moving piece: ', data); @@ -87,7 +98,7 @@ const ChessGameContextProvider = ({ children }) => { } // called when user clicks a square - function handleSquareClick(square) { + function handleSquareClick(square, emitToSocketCallback) { let { type, color } = chess.get(square); let marked = moveHints.includes(square); console.log('handleSquareClick', square) @@ -99,9 +110,13 @@ const ChessGameContextProvider = ({ children }) => { } if (!type && selected && marked) { dispatch({ type: MOVE_PIECE, val: { from: selected, to: square } }) + emitToSocketCallback({ from: selected, to: square }) + captureAudioRef.current.play(); } if (type && marked) { dispatch({ type: CAPTURE_PIECE, val: { from: selected, to: square } }) + emitToSocketCallback({ from: selected, to: square }) + moveAudioRef.current.play(); } } else { return; @@ -165,9 +180,14 @@ const ChessGameContextProvider = ({ children }) => { } } + // fetchedGameHistory is an array of objects of the form {from,to} + function setGameHistory(fetchedGameHistory) { + dispatch({ type: SET_GAME_HISTORY, val: fetchedGameHistory }) + } + return ( {children}