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)}
- />
-
+
-
+
+
setCreate(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);
});
});