207 lines
5.8 KiB
TypeScript
207 lines
5.8 KiB
TypeScript
import wp from "../assets/pieces/wp.png";
|
|
import wn from "../assets/pieces/wn.png";
|
|
import wb from "../assets/pieces/wb.png";
|
|
import wr from "../assets/pieces/wr.png";
|
|
import wq from "../assets/pieces/wq.png";
|
|
import wk from "../assets/pieces/wk.png";
|
|
|
|
import bp from "../assets/pieces/bp.png";
|
|
import bn from "../assets/pieces/bn.png";
|
|
import bb from "../assets/pieces/bb.png";
|
|
import br from "../assets/pieces/br.png";
|
|
import bq from "../assets/pieces/bq.png";
|
|
import bk from "../assets/pieces/bk.png";
|
|
|
|
import Chess, {
|
|
squareColor,
|
|
Piece,
|
|
PieceType,
|
|
Color,
|
|
algebraic,
|
|
file,
|
|
rank,
|
|
FILE,
|
|
RANK,
|
|
squareIndex,
|
|
Move,
|
|
MOVE_FLAGS,
|
|
PIECE_PROMOTION,
|
|
PiecePromotionType,
|
|
} from "../../../server/src/chess/engine";
|
|
import { MouseEventHandler, useState } from "react";
|
|
import Show from "../utils/Show";
|
|
import useWindowSize from "../utils/useWindowSize";
|
|
|
|
|
|
const PIECES: Record<Color, Record<PieceType, string>> = {
|
|
w: { p: wp, n: wn, b: wb, r: wr, q: wq, k: wk },
|
|
b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk },
|
|
};
|
|
const ChessBoard: React.FC<{
|
|
chess: Chess;
|
|
sendMove: (move: Move) => void;
|
|
blackPerspective?: boolean;
|
|
}> = ({ chess, sendMove, blackPerspective }) => {
|
|
const { width, height } = useWindowSize();
|
|
const [activeTile, setActiveTile] = useState<number>(-1);
|
|
const [promotionMove, setPromotionMove] = useState<
|
|
(Pick<Move, "from" | "to"> & { color: Color }) | null
|
|
>(null);
|
|
|
|
const gridSize = Math.min(width, height, 800);
|
|
|
|
const tileProps = new Array(64).fill(null).map((_, i) => ({
|
|
tileNumber: i,
|
|
piece: chess.getPiece(i),
|
|
isAttacked: false,
|
|
isPromotion: false,
|
|
isActive: false,
|
|
}));
|
|
|
|
if (activeTile != -1) {
|
|
tileProps[activeTile].isActive = true;
|
|
chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => {
|
|
const square = squareIndex(to);
|
|
tileProps[square].isAttacked = true;
|
|
tileProps[square].isPromotion =
|
|
flags & MOVE_FLAGS.PROMOTION ? true : false;
|
|
});
|
|
}
|
|
|
|
const tiles = tileProps.map((props, i) => (
|
|
<Tile key={i} {...props} blackPerspective={blackPerspective} />
|
|
));
|
|
|
|
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
|
if (promotionMove != null) return;
|
|
|
|
// TODO: fix types
|
|
// @ts-ignore
|
|
const tile = parseInt(e.target.dataset.tile);
|
|
|
|
if (isNaN(tile)) return;
|
|
|
|
if (activeTile != -1 && tileProps[tile].isAttacked) {
|
|
const moveObj = {
|
|
from: algebraic(activeTile),
|
|
to: algebraic(tile),
|
|
color: (chess.getPiece(activeTile) as Piece).color,
|
|
};
|
|
|
|
if (tileProps[tile].isPromotion) return setPromotionMove(moveObj);
|
|
|
|
sendMove(moveObj);
|
|
setActiveTile(-1);
|
|
}
|
|
|
|
if (tileProps[tile].piece == null) return setActiveTile(-1);
|
|
|
|
if (tile == activeTile) setActiveTile(-1);
|
|
else setActiveTile(tile);
|
|
};
|
|
|
|
const sendPromotionMove = (promotion: PiecePromotionType) => {
|
|
if (promotionMove == null) return;
|
|
|
|
const moveObj: Move = {
|
|
from: promotionMove.from,
|
|
to: promotionMove.to,
|
|
promotion,
|
|
};
|
|
|
|
sendMove(moveObj);
|
|
setPromotionMove(null);
|
|
setActiveTile(-1);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-row h-fit items-center">
|
|
<div
|
|
className="relative grid grid-rows-8 grid-cols-8 aspect-square border-8 border-black rounded-lg"
|
|
onClick={handleClick}
|
|
style={{ width: `${gridSize}px` }}
|
|
>
|
|
{blackPerspective == true ? tiles.reverse() : tiles}
|
|
</div>
|
|
{promotionMove == null ? (
|
|
<></>
|
|
) : (
|
|
<div className="w-fit flex flex-col bg-black rounded-r-2xl p-1">
|
|
{PIECE_PROMOTION.map((p) => (
|
|
<img
|
|
key={p}
|
|
src={PIECES[promotionMove.color][p]}
|
|
style={{ width: `${gridSize / 8}px`, aspectRatio: 1 }}
|
|
onClick={() => sendPromotionMove(p)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TILE_COLORS = Object.freeze({
|
|
w: {
|
|
bg: "bg-white-tile",
|
|
text: "text-black-tile",
|
|
active: "bg-sky-400",
|
|
},
|
|
b: {
|
|
bg: "bg-black-tile",
|
|
text: "text-white-tile",
|
|
active: "bg-sky-700",
|
|
},
|
|
} as const);
|
|
|
|
const Tile: React.FC<{
|
|
tileNumber: number;
|
|
piece: Piece | null;
|
|
isActive: boolean;
|
|
isAttacked: boolean;
|
|
blackPerspective?: boolean;
|
|
}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => {
|
|
const color = squareColor(tileNumber);
|
|
|
|
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
|
|
const activeColor = isActive ? TILE_COLORS[color].active : "";
|
|
|
|
const tileFile = file(tileNumber);
|
|
const tileRank = rank(tileNumber);
|
|
const square = algebraic(tileNumber);
|
|
|
|
const isFirstColumn = tileFile == (blackPerspective ? FILE.H : FILE.A);
|
|
const isLastRow = tileRank == (blackPerspective ? RANK.EIGHTH : RANK.FIRST);
|
|
|
|
return (
|
|
<div
|
|
className={`relative aspect-square font-bold text-xl isolate group [&>*]:pointer-events-none ${bgColor} ${activeColor}`}
|
|
data-tile={tileNumber}
|
|
>
|
|
{piece == null ? <></> : <img src={PIECES[piece.color][piece.type]} />}
|
|
<Show when={isAttacked == true}>
|
|
<Show
|
|
when={piece == null}
|
|
fallback={
|
|
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-full aspect-square border-8 border-gray-900/40 rounded-full"></div>
|
|
}
|
|
>
|
|
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-[35%] aspect-square bg-gray-900/40 rounded-full group-hover:w-[45%]"></div>
|
|
</Show>
|
|
</Show>
|
|
<Show when={isFirstColumn}>
|
|
<div className={`absolute top-1 left-1 -z-10 ${textColor}`}>
|
|
{square[1]}
|
|
</div>
|
|
</Show>
|
|
<Show when={isLastRow}>
|
|
<div className={`absolute bottom-1 right-1 -z-10 ${textColor}`}>
|
|
{square[0]}
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChessBoard;
|