Files
ChessHub/frontend/src/context/chess-game-context.jsx
T
Moon Patel 59af2e756a refactored reducer function in chess-game-context.sx
The reducer function in chess-game-context.jsx previously accepted two different action types MOVE_PIECE and
CAPTURE_PIECE. Due to this the caller has to verify whether a move is a simple move or a capture. Since, the dispatch
was called in multilpe places, it is better to keep one action type MOVE_PIECE and move the differentiating logic
to the reducer. Also this commit implements a playAudioCallback which receives an action string and plays an audio
accordingly. This is passed to the dispatch function and it is the responsibility of the reducer to call it with
an appropriate action.
2023-10-20 16:55:03 +05:30

247 lines
10 KiB
React

import React, { createContext, useReducer, useRef, useState } from 'react'
import PropTypes from 'prop-types';
import { socket } from '../socket';
import { ChessModified, chessInit } from '../utils/chess';
import { DISPATCH_EVENTS, SOCKET_EVENTS } from '../constants';
const { MOVE_PIECE, SELECT_PIECE, JUMP_TO, SET_GAME_HISTORY, END_GAME } = DISPATCH_EVENTS
const { GAME_END } = SOCKET_EVENTS;
export const ChessGameContext = createContext();
const reducer = (state, action) => {
try {
switch (action.type) {
case SELECT_PIECE:
{
if (state.chess.turn() === state.chess.myColor && state.currentIndex < state.gameHistory.length - 1) return { ...state, currentIndex: state.gameHistory.length - 1 }
return { ...state, moveHints: state.chess.getMoves(action.val), selected: action.val };
}
case MOVE_PIECE:
{
let newChessObj = new ChessModified(state.chess.fen());
let updatedGameHistory = state.gameHistory;
let { san, after } = newChessObj.move(action.val);
updatedGameHistory.push({ move: san, fen: after });
let newState;
if (newChessObj.isCheckmate()) {
action.val.callback();
action.val.playAudioCallback("CHECKMATE");
newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'CHECKMATE' };
} else if(newChessObj.isCheck() || newChessObj.inCheck()) {
action.val.playAudioCallback("CHECK");
newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 };
} else if (newChessObj.isStalemate()) {
action.val.callback();
action.val.playAudioCallback("STALEMATE");
newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1, hasGameEnded: true, gameEndedReason: 'STALEMATE' };
}
else {
if(!state.chess.get(action.val.to)) {
action.val.playAudioCallback("MOVE");
} else {
action.val.playAudioCallback("CAPTURE");
}
newState = { ...state, chess: newChessObj, chessBoard: newChessObj.getBoard(), moveHints: [], selected: null, gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 };
}
return newState;
}
case JUMP_TO:
{
let index = action.val;
return { ...state, currentIndex: index }
}
case SET_GAME_HISTORY:
{
let fetchedGameHistory = action.val;
let newChessObj = new ChessModified();
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(), gameHistory: updatedGameHistory, currentIndex: updatedGameHistory.length - 1 }
}
case END_GAME:
{
return { ...state, hasGameEnded: true, gameEndedReason: action.val }
}
default:
return state;
}
} catch (err) {
console.error(err);
return state;
}
}
function chessGameStateInit(myColor) {
let chess = chessInit(myColor);
let chessBoard = chess.getBoard();
let moveHints = [];
let gameHistory = [];
let selected = null;
let currentIndex = -1;
let hasGameEnded = false;
let gameEndedReason = "";
return { chess, chessBoard, moveHints, selected, gameHistory, currentIndex, hasGameEnded, gameEndedReason };
}
// the ChessGameContextProvider seperates the game logic from the ChessBoard component and exposes
// some functions to update game state.
const ChessGameContextProvider = ({ children }) => {
let myColor = localStorage.getItem('myColor');
let roomID = localStorage.getItem('roomID');
const [{ chess, chessBoard, moveHints, selected, gameHistory, currentIndex, hasGameEnded, gameEndedReason }, dispatch] = useReducer(reducer, myColor, chessGameStateInit);
const [isTimerOn, setIsTimerOn] = useState(true);
const chessRef = useRef(chess);
const moveHintsRef = useRef(moveHints);
const selectedRef = useRef(selected);
const gameHistoryRef = useRef(gameHistory);
const currentIndexRef = useRef(currentIndex);
chessRef.current = chess;
selectedRef.current = selected;
moveHintsRef.current = moveHints;
gameHistoryRef.current = gameHistory;
currentIndexRef.current = currentIndex;
const moveAudioRef = useRef(null);
const captureAudioRef = useRef(null);
const gameEndAudioRef = useRef(null);
const checkAudioRef = useRef(null);
function playAudioCallback(action) {
switch(action) {
case "MOVE":
moveAudioRef.current.play();
break;
case "CAPTURE":
captureAudioRef.current.play();
break;
case "CHECK":
checkAudioRef.current.play();
break;
case "CHECKMATE":
case "STALEMATE":
case "GAME_END":
gameEndAudioRef.current.play();
break;
case "CASTLE":
break;
default:
break;
}
}
// data - received through socket
function handleOpponentMove(data, callback) {
let { from, to } = data;
console.log("Opponent move:",from,to);
dispatch({type:MOVE_PIECE,val: { from, to, callback,playAudioCallback }});
}
// called when user clicks a square
function handleSquareClick(square, emitToSocketCallback, callback) {
let { type, color } = chessRef.current.get(square);
let marked = moveHintsRef.current.includes(square);
if (chessRef.current.turn() === myColor) {
if (type && color === myColor) {
selectPiece({square,color});
return;
} else if(marked) {
dispatch({ type: MOVE_PIECE, val: { from: selectedRef.current, to: square, callback,playAudioCallback } })
console.log("Move:",{ from: selectedRef.current, to: square })
emitToSocketCallback({ from: selectedRef.current, to: square })
}
}
}
function handleDrop(moveData, emitToSocketCallback, callback) {
let { from, to } = moveData;
if (moveHintsRef.current.includes(to)) {
dispatch({ type: MOVE_PIECE, val: { from: from, to: to, callback,playAudioCallback } }); // capture piece
console.log("Move:",{ from,to })
emitToSocketCallback(moveData);
}
}
function selectPiece({ square, color: pieceColor }) {
if (pieceColor === myColor && myColor === chessRef.current.turn()) {
dispatch({ type: SELECT_PIECE, val: square });
}
}
function getSquareColor(square) {
return chessRef.current.squareColor(square) === 'light' ? "w" : "b";
}
function isSquareMarked(square) {
return moveHintsRef.current.includes(square);
}
function jumpTo(index) {
dispatch({ type: JUMP_TO, val: index })
}
function getChessBoard() {
if (currentIndexRef.current === -1 || gameHistoryRef.current.length === 0) {
return new ChessModified().getBoard();
} else {
// console.log(chess);
let currentChessBoard = new ChessModified(gameHistoryRef.current[currentIndexRef.current].fen).getBoard();
return currentChessBoard;
}
}
function goBack() {
if (currentIndexRef.current > 0) {
jumpTo(currentIndexRef.current - 1);
}
}
function goAhead() {
if (currentIndexRef.current < gameHistoryRef.current.length - 1) {
jumpTo(currentIndexRef.current + 1);
}
}
// fetchedGameHistory is an array of objects of the form {from,to}
function setGameHistory(fetchedGameHistory) {
dispatch({ type: SET_GAME_HISTORY, val: fetchedGameHistory })
}
function endGame(reason) {
dispatch({ type: END_GAME, val: reason })
socket.emit(GAME_END, roomID);
}
function getPieceColor(square) {
return chessRef.current.get(square).color
}
return (
<ChessGameContext.Provider value={{
myColor, chess, chessBoard, moveHints, selected, handleOpponentMove, handleSquareClick, getSquareColor, isSquareMarked,
selectPiece, handleDrop, gameHistory, jumpTo, getChessBoard, currentIndex, goAhead, goBack, setGameHistory,
isTimerOn, hasGameEnded, gameEndedReason, endGame,getPieceColor
}}>
{children}
<audio src='/src/assets/audio/move-self.mp3' ref={moveAudioRef} />
<audio src='/src/assets/audio/capture.mp3' ref={captureAudioRef} />
<audio src='/src/assets/audio/game-end.webm' ref={gameEndAudioRef} />
<audio src='/src/assets/audio/move-check.mp3' ref={checkAudioRef} />
</ChessGameContext.Provider>
)
}
ChessGameContextProvider.propTypes = {
children: PropTypes.object
}
export default ChessGameContextProvider