multiple changes

- add gameover overlay
- fix active tile background
- redirect to home on invalid page
- compute checkmate and draw after making a move
- cleanup code
- error handling on making moves
This commit is contained in:
Cozma Rares
2023-07-31 01:24:26 +03:00
parent 259393ca1b
commit 0b68d701c1
8 changed files with 188 additions and 140 deletions
+5 -2
View File
@@ -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 (
<div
className={`relative aspect-square font-bold text-xl isolate group [&>*]: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 ? <></> : <img src={PIECES[piece.color][piece.type]} />}
+21 -2
View File
@@ -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: <Game />,
},
{
path: "*",
element: <ErrorElement />,
},
]);
// React's StricMode doesn't play well with how I implemented the socket connection
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<RouterProvider router={router} />
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-zinc-200 -z-10">
<RouterProvider router={router} />
</div>
);
+19 -3
View File
@@ -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<Error>();
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()}
/>
<Show when={chess.isGameOver()}>
<Modal overlay>
<div className="text-center w-40 max-w-full">
<h2 className="text-2xl mb-2">Game Over</h2>
<h1 className="text-lg font-bold">
{chess.isCheckMate()
? (chess.getTurn() == color ? "Opponent" : "You") + " won!"
: "It's a draw!"}
</h1>
<div className="w-full h-[4px] rounded-b-lg bg-white mb-4"></div>
<ModalButton onClick={() => navigate("/")}>
Go to main page
</ModalButton>
</div>
</Modal>
</Show>
</Show>
);
};
+55 -63
View File
@@ -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<string>("");
const [join, setJoin] = useState(false);
const [color, setColor] = useState<Color>(COLOR.WHITE);
const [join, setJoin] = useState(false);
const [create, setCreate] = useState(false);
const [err, setErr] = useState<Error>();
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 (
<>
<ErrorNorification key={errObj.error} {...errObj} />
<div className="text-xl">
<div className="absolute top-0 left-0 right-0 bottom-0 m-auto p-6 bg-gray-800 text-white w-fit h-fit rounded-[25px]">
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mb-3"
onClick={createGame}
>
Create Game
</button>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors"
onClick={() => setJoin(true)}
>
Join Game
</button>
<div className="absolute top-0 left-0 right-0 bottom-0 m-auto w-fit h-fit">
<Modal>
<ModalButton onClick={() => setCreate(true)}>Create Game</ModalButton>
<ModalButton onClick={() => setJoin(true)}>Join Game</ModalButton>
</Modal>
</div>
<Show when={join == false && id != ""}>
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex justify-center items-center bg-zinc-800 bg-opacity-70">
<div className="bg-gray-900 text-white p-4 rounded-lg">
<Show when={create}>
<Modal overlay>
<div className="w-[20rem] max-w-full">
<p>Select color:</p>
<div className="grid grid-cols-[auto,minmax(0,1fr)] text-center ">
<input
type="radio"
name="color"
id="white"
checked={color == COLOR.WHITE}
onChange={() => setColor(COLOR.WHITE)}
/>
<label htmlFor="white">White</label>
<input
type="radio"
name="color"
id="black"
checked={color == COLOR.BLACK}
onChange={() => setColor(COLOR.BLACK)}
/>
<label htmlFor="black">Black</label>
<div className="flex flex-row justify-evenly">
<div className="flex flex-row items-center gap-2">
<input
type="radio"
name="color"
id="white"
checked={color == COLOR.WHITE}
onChange={() => setColor(COLOR.WHITE)}
/>
<label htmlFor="white">White</label>
</div>
<div className="flex flex-row items-center gap-2">
<input
type="radio"
name="color"
id="black"
checked={color == COLOR.BLACK}
onChange={() => setColor(COLOR.BLACK)}
/>
<label htmlFor="black">Black</label>
</div>
</div>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mt-3"
onClick={() => navigate("/game", { state: { id, color } })}
<ModalButton
onClick={createGame}
>
Start Game
</button>
</ModalButton>
<ModalButton
onClick={() => setCreate(false)}
>
Cancel
</ModalButton>
</div>
</div>
</Modal>
</Show>
<Show when={join}>
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex justify-center items-center bg-zinc-800 bg-opacity-70">
<div className="bg-gray-900 text-white p-4 rounded-lg">
<Modal overlay>
<div className="w-[20rem] max-w-full">
<label htmlFor="game-id">Insert game ID: </label>
<input
className="block bg-zinc-700 p-1 mt-2"
className="block bg-zinc-700 p-1 mt-2 w-full"
type="text"
name="game-id"
id="game-id"
value={id}
onChange={(e) => setID(e.target.value)}
/>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mt-3"
onClick={joinGame}
>
Join
</button>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mt-3"
onClick={() => setJoin(false)}
>
Cancel
</button>
<ModalButton onClick={joinGame}>Join</ModalButton>
<ModalButton onClick={() => setJoin(false)}>Cancel</ModalButton>
</div>
</div>
</Modal>
</Show>
</div>
</>
+33
View File
@@ -0,0 +1,33 @@
import Show from "./Show";
const Modal: React.FC<{
children: React.ReactNode;
overlay?: boolean;
}> = ({ children, overlay }) => {
const modal = (
<div className="bg-gray-900 text-white p-4 rounded-lg w-fit">
{children}
</div>
);
return overlay ? (
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex justify-center items-center bg-zinc-800 bg-opacity-70">
{modal}
</div>
) : (
modal
);
};
export const ModalButton: React.FC<{
children: React.ReactNode;
onClick: () => void;
}> = ({ children, onClick }) => (
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors [&:nth-child(n+2)]:mt-3"
onClick={onClick}
>
{children}
</button>
);
export default Modal;
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "server",
"name": "budget chess.com",
"version": "1.0.0",
"description": "",
"main": "index.js",
+48 -67
View File
@@ -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();
+6 -2
View File
@@ -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);
});
});