diff --git a/client/src/components/Chessboard.tsx b/client/src/components/Chessboard.tsx index a8dae2e..1cfe37c 100644 --- a/client/src/components/Chessboard.tsx +++ b/client/src/components/Chessboard.tsx @@ -169,7 +169,7 @@ const Tile: React.FC<{ const color = squareColor(tileNumber); const { bg: bgColor, text: textColor } = TILE_COLORS[color]; - const activeColor = isActive ? TILE_COLORS[color].active : ""; + const activeColor = TILE_COLORS[color].active; const tileFile = file(tileNumber); const tileRank = rank(tileNumber); @@ -180,7 +180,10 @@ const Tile: React.FC<{ return (
*]:pointer-events-none ${bgColor} ${activeColor}`} + className={ + "relative aspect-square font-bold text-xl isolate group [&>*]:pointer-events-none " + + (isActive ? activeColor : bgColor) + } data-tile={tileNumber} > {piece == null ? <> : } diff --git a/client/src/main.tsx b/client/src/main.tsx index 9a7a398..a64d105 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,8 +1,21 @@ import ReactDOM from "react-dom/client"; import "./index.css"; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { + createBrowserRouter, + RouterProvider, + useNavigate, +} from "react-router-dom"; import Home from "./routes/Home"; import Game from "./routes/Game"; +import { useEffect } from "react"; + +const ErrorElement = () => { + const naviagte = useNavigate(); + + useEffect(() => naviagte("/"), []); + + return <>; +}; const router = createBrowserRouter([ { @@ -13,9 +26,15 @@ const router = createBrowserRouter([ path: "/game", element: , }, + { + path: "*", + element: , + }, ]); // React's StricMode doesn't play well with how I implemented the socket connection ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - +
+ +
); diff --git a/client/src/routes/Game.tsx b/client/src/routes/Game.tsx index f3f9da6..7f25b38 100644 --- a/client/src/routes/Game.tsx +++ b/client/src/routes/Game.tsx @@ -8,6 +8,7 @@ import { socket } from "../sockets/socket"; import { CopyIcon } from "../utils/icons"; import Show from "../utils/Show"; import ErrorNorification from "../utils/ErrorNotification"; +import Modal, { ModalButton } from "../utils/Modal"; const Game = () => { const navigate = useNavigate(); @@ -18,7 +19,7 @@ const Game = () => { const [chess] = useState(Chess.load()); const [game, setGame] = useState(false); - const [, setRerender] = useState(false); + const [, rerender] = useState(false); const [err, setErr] = useState(); const makeMove = (move: Move) => socket.emit("make move", id, move); @@ -36,7 +37,6 @@ const Game = () => { }); socket.on("join error", () => { - socket.disconnect(); navigate("/", { state: { error: "Could not join, please try again." }, }); @@ -50,7 +50,7 @@ const Game = () => { socket.on("receive move", (move: Move) => { chess.makeMove(move); - setRerender((prev) => !prev); + rerender((prev) => !prev); }); return () => { @@ -71,6 +71,22 @@ const Game = () => { blackPerspective={color === COLOR.BLACK} disabled={color !== chess.getTurn()} /> + + +
+

Game Over

+

+ {chess.isCheckMate() + ? (chess.getTurn() == color ? "Opponent" : "You") + " won!" + : "It's a draw!"} +

+
+ navigate("/")}> + Go to main page + +
+
+
); }; diff --git a/client/src/routes/Home.tsx b/client/src/routes/Home.tsx index 0028c97..d7761e5 100644 --- a/client/src/routes/Home.tsx +++ b/client/src/routes/Home.tsx @@ -3,14 +3,16 @@ import { useLocation, useNavigate } from "react-router-dom"; import { Color, COLOR } from "../../../server/src/engine"; import Show from "../utils/Show"; import ErrorNorification from "../utils/ErrorNotification"; +import Modal, { ModalButton } from "../utils/Modal"; const Home = () => { const navigate = useNavigate(); const location = useLocation(); const [id, setID] = useState(""); - const [join, setJoin] = useState(false); const [color, setColor] = useState(COLOR.WHITE); + const [join, setJoin] = useState(false); + const [create, setCreate] = useState(false); const [err, setErr] = useState(); const { error } = @@ -22,8 +24,8 @@ const Home = () => { if (res.ok) return res.text(); throw res; }) - .then((id) => { - setID(id); + .then((text) => { + navigate("/game", { state: { id: text, color } }); }) .catch((err) => { console.error(err); @@ -56,89 +58,79 @@ const Home = () => { const errObj = err?.message ? { - error: err.message, - removeError: () => setErr(undefined), - } + error: err.message, + removeError: () => setErr(undefined), + } : { - error, - removeError: removeLocationState, - }; + error, + removeError: removeLocationState, + }; return ( <>
-
- - +
+ + setCreate(true)}>Create Game + setJoin(true)}>Join Game +
- -
-
+ + +

Select color:

-
- setColor(COLOR.WHITE)} - /> - - setColor(COLOR.BLACK)} - /> - +
+
+ setColor(COLOR.WHITE)} + /> + +
+
+ setColor(COLOR.BLACK)} + /> + +
- + + setCreate(false)} + > + Cancel +
-
+
-
-
+ +
setID(e.target.value)} /> - - + Join + setJoin(false)}>Cancel
-
+
diff --git a/client/src/utils/Modal.tsx b/client/src/utils/Modal.tsx new file mode 100644 index 0000000..703bf1f --- /dev/null +++ b/client/src/utils/Modal.tsx @@ -0,0 +1,33 @@ +import Show from "./Show"; + +const Modal: React.FC<{ + children: React.ReactNode; + overlay?: boolean; +}> = ({ children, overlay }) => { + const modal = ( +
+ {children} +
+ ); + + return overlay ? ( +
+ {modal} +
+ ) : ( + modal + ); +}; +export const ModalButton: React.FC<{ + children: React.ReactNode; + onClick: () => void; +}> = ({ children, onClick }) => ( + +); + +export default Modal; diff --git a/package.json b/package.json index 8af9531..44277de 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "server", + "name": "budget chess.com", "version": "1.0.0", "description": "", "main": "index.js", diff --git a/server/src/engine.ts b/server/src/engine.ts index 784c4b8..e65518e 100644 --- a/server/src/engine.ts +++ b/server/src/engine.ts @@ -201,7 +201,7 @@ export function validateFEN(fen: string): void { if (!isPieceValid(symbol.toLowerCase())) throw new Error( "Invalid FEN - board position contains an invalid piece symbol: " + - symbol + symbol ); numSquares++; @@ -504,6 +504,8 @@ export default class Chess { private _attacks: number[] = []; private _history: string[] = []; private _enableProcessMoves = true; + private _checkMate = false; + private _draw = false; private constructor( board: Board, @@ -523,7 +525,7 @@ export default class Chess { this._fullMoves = fullMoves; this._kings = kings; this._enableProcessMoves = enableProcessMoves; - this._computeMoves(); + this._processBoardState(); } private _computeMoves() { @@ -588,7 +590,17 @@ export default class Chess { } } - static load(fen = DEFAULT_POSITION, enableProcessMoves = true) { + private _processBoardState() { + this._computeMoves(); + this._checkMate = this.isCheck() && this._moves.length === 0; + this._draw = + this._halfMoves >= 100 || // 50 moves per side = 100 half moves + this.isStalemate() || + this.isInsufficientMaterial() || + this.isThreefoldRepetition(); + } + + private static _load(fen: string, enableProcessMoves: boolean) { validateFEN(fen); const builder = new Chess.Builder(); @@ -622,6 +634,10 @@ export default class Chess { return builder.build(); } + static load(fen = DEFAULT_POSITION) { + return this._load(fen, true); + } + reset() { const chess = Chess.load(); this._board = chess._board; @@ -724,7 +740,7 @@ export default class Chess { private _processMoves() { const currentFEN = this.getFEN(); this._moves = this._moves.filter((move) => { - const chess = Chess.load(currentFEN, false); + const chess = Chess._load(currentFEN, false); chess._makeMove(move); return !chess._isKingAttacked(this._turn); }, this); @@ -743,9 +759,9 @@ export default class Chess { this._board[squareIndex(move.to)] = move.flags & MOVE_FLAGS.PROMOTION ? { - type: move.promotion as PieceType, - color: myColor, - } + type: move.promotion as PieceType, + color: myColor, + } : this._board[squareIndex(move.from)]; this._board[squareIndex(move.from)] = null; let keepEpSquare = false; @@ -800,9 +816,7 @@ export default class Chess { this._turn = theirColor; this._updateCastling(); - - if (this.isGameOver()) this._moves = []; - else this._computeMoves(); + this._processBoardState(); } makeMove(move: Move) { @@ -820,69 +834,41 @@ export default class Chess { } } - if (moveObj == null) - throw new Error("Move not found"); + if (moveObj == null) throw new Error("Move not found"); this._makeMove(moveObj); } private _updateCastling() { - const forWhite = () => { - const castling = this._castling[COLOR.WHITE]; + const uptate = (color: Color) => { + const castling = this._castling[color]; if (castling == 0) return; - const whiteKing = this.getPiece("e1"); + const king = this.getPiece(color == COLOR.WHITE ? "e1" : "e8"); - if (whiteKing?.type != PIECE.KING && whiteKing?.color != COLOR.WHITE) { - this._castling[COLOR.WHITE] = 0; + if (king?.type != PIECE.KING && king?.color != color) { + this._castling[color] = 0; return; } if (castling & MOVE_FLAGS.K_CASTLE) { - const rook = this.getPiece("h1"); + const rook = this.getPiece(color == COLOR.WHITE ? "h1" : "h8"); - if (rook?.type != PIECE.ROOK && rook?.color != COLOR.WHITE) - this._castling[COLOR.WHITE] ^= MOVE_FLAGS.K_CASTLE; + if (rook?.type != PIECE.ROOK && rook?.color != color) + this._castling[color] ^= MOVE_FLAGS.K_CASTLE; } if (castling & MOVE_FLAGS.Q_CASTLE) { - const rook = this.getPiece("a1"); + const rook = this.getPiece(color == COLOR.WHITE ? "a1" : "a8"); - if (rook?.type != PIECE.ROOK && rook?.color != COLOR.WHITE) - this._castling[COLOR.WHITE] ^= MOVE_FLAGS.Q_CASTLE; + if (rook?.type != PIECE.ROOK && rook?.color != color) + this._castling[color] ^= MOVE_FLAGS.Q_CASTLE; } }; - const forBlack = () => { - const castling = this._castling[COLOR.BLACK]; - - if (castling == 0) return; - - const blackKing = this.getPiece("e8"); - - if (blackKing?.type != PIECE.KING && blackKing?.color != COLOR.BLACK) { - this._castling[COLOR.BLACK] = 0; - return; - } - - if (castling & MOVE_FLAGS.K_CASTLE) { - const rook = this.getPiece("h8"); - - if (rook?.type != PIECE.ROOK && rook?.color != COLOR.BLACK) - this._castling[COLOR.BLACK] ^= MOVE_FLAGS.K_CASTLE; - } - - if (castling & MOVE_FLAGS.Q_CASTLE) { - const rook = this.getPiece("a8"); - - if (rook?.type != PIECE.ROOK && rook?.color != COLOR.BLACK) - this._castling[COLOR.BLACK] ^= MOVE_FLAGS.Q_CASTLE; - } - }; - - forWhite(); - forBlack(); + uptate(COLOR.WHITE); + uptate(COLOR.BLACK); } isSquareAttacked(square: Square | number, attackedBy: Color) { @@ -892,7 +878,7 @@ export default class Chess { : (this._attacks[square] & COLOR_MASKS[attackedBy]) != 0; } - _isKingAttacked(color: Color) { + private _isKingAttacked(color: Color) { const square = this._kings[color]; return this.isSquareAttacked(square, swapColor(color)); } @@ -902,7 +888,7 @@ export default class Chess { } isCheckMate() { - return this.isCheck() && this._moves.length === 0; + return this._checkMate; } isStalemate() { @@ -934,10 +920,10 @@ export default class Chess { remainingPieces[PIECE.BISHOP][COLOR.WHITE] == 2 || remainingPieces[PIECE.BISHOP][COLOR.BLACK] == 2 || remainingPieces[PIECE.BISHOP][COLOR.WHITE] + - remainingPieces[PIECE.BISHOP][COLOR.BLACK] + - remainingPieces[PIECE.KNIGHT][COLOR.WHITE] + - remainingPieces[PIECE.KNIGHT][COLOR.BLACK] >= - 3 + remainingPieces[PIECE.BISHOP][COLOR.BLACK] + + remainingPieces[PIECE.KNIGHT][COLOR.WHITE] + + remainingPieces[PIECE.KNIGHT][COLOR.BLACK] >= + 3 ) return false; @@ -950,12 +936,7 @@ export default class Chess { } isDraw() { - return ( - this._halfMoves >= 100 || // 50 moves per side = 100 half moves - this.isStalemate() || - this.isInsufficientMaterial() || - this.isThreefoldRepetition() - ); + return this._draw; } isGameOver() { @@ -967,7 +948,7 @@ export default class Chess { const lastFen = this._history.pop(); if (lastFen == undefined) return; - const chess = Chess.load(lastFen, this._enableProcessMoves); + const chess = Chess._load(lastFen, this._enableProcessMoves); this._board = chess._board; this._turn = chess._turn; this._castling = chess._castling; @@ -975,7 +956,7 @@ export default class Chess { this._halfMoves = chess._halfMoves; this._fullMoves = chess._fullMoves; this._kings = chess._kings; - this._computeMoves(); + this._processBoardState(); } getTurn() { @@ -983,7 +964,7 @@ export default class Chess { } // TODO: implement - history() { } + history() {} toString() { return this.getFEN(); diff --git a/server/src/server.ts b/server/src/server.ts index b6bbbbe..cc9a820 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -42,6 +42,7 @@ io.on("connection", (socket) => { const room = rooms.get(id); if ( + socket.rooms.size >= 2 || room == undefined || (room[color] != null && room[color] != socket.id) ) { @@ -69,10 +70,13 @@ io.on("connection", (socket) => { try { chess.makeMove(move); - io.to(id).emit("receive move", move); } catch (e) { - socket.emit("move error", (e as Error).message); + return socket.emit("move error", (e as Error).message); } + + io.to(id).emit("receive move", move); + + if (chess.isGameOver()) rooms.delete(id); }); });