fix: coordinate letters, formatting

This commit is contained in:
Cozma Rares
2023-09-15 18:37:02 +03:00
parent de73210278
commit e7d236ea68
16 changed files with 323 additions and 318 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};
+1 -1
View File
@@ -8,7 +8,7 @@ import Modal, { ModalButton } from "./Modal";
import { useState } from "react";
const ChessUI: React.FC<InferProps<[typeof ChessBoard, typeof History]>> = (
props
props,
) => {
const [showModal, setShowModal] = useState(true);
const navigate = useNavigate();
+53 -51
View File
@@ -121,7 +121,7 @@ const ChessBoard: React.FC<{
if (tile == activeTile) return setActiveTile(-1);
setActiveTile(
chess.getMovesForSquare(algebraic(tile)).length == 0 ? -1 : tile
chess.getMovesForSquare(algebraic(tile)).length == 0 ? -1 : tile,
);
};
@@ -198,60 +198,62 @@ const Tile: React.FC<{
blackPerspective,
endGameIcon,
}) => {
const color = squareColor(tileNumber);
const color = squareColor(tileNumber);
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
const activeColor = TILE_COLORS[color].active;
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
const activeColor = TILE_COLORS[color].active;
const tileFile = file(tileNumber);
const tileRank = rank(tileNumber);
const square = algebraic(tileNumber);
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);
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 group [&>*]:pointer-events-none outline-blue-300 hover:outline outline-2 sm:outline-4 -outline-offset-2 sm:-outline-offset-4",
isActive ? activeColor : bgColor,
piece ? "cursor-pointer" : "",
].join(" ")}
data-tile={tileNumber}
>
{piece && (
<img
src={PIECES[piece.color][piece.type]}
className="max-w-full max-h-full aspect-square"
/>
)}
<Show when={endGameIcon != undefined}>
<div className="absolute top-0 left-full -translate-x-1/2 -translate-y-1/2 bg-white z-10 rounded-full p-1 shadow-lg shadow-black aspect-square flex justify-center items-center">
{endGameIcon}
</div>
if (isFirstColumn || isLastRow) console.log(square);
return (
<div
className={[
"relative isolate aspect-square font-bold text-xl group [&>*]:pointer-events-none outline-blue-300 hover:outline outline-2 sm:outline-4 -outline-offset-2 sm:-outline-offset-4",
isActive ? activeColor : bgColor,
piece ? "cursor-pointer" : "",
].join(" ")}
data-tile={tileNumber}
>
{piece && (
<img
src={PIECES[piece.color][piece.type]}
className="max-w-full max-h-full aspect-square"
/>
)}
<Show when={endGameIcon != undefined}>
<div className="absolute top-0 left-full -translate-x-1/2 -translate-y-1/2 bg-white z-10 rounded-full p-1 shadow-lg shadow-black aspect-square flex justify-center items-center">
{endGameIcon}
</div>
</Show>
<Show when={isAttacked}>
<Show
when={piece == null}
fallback={
<div className="absolute z-10 -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 z-10 -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 when={isAttacked}>
<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>
);
};
</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;
+1 -1
View File
@@ -16,7 +16,7 @@ const ErrorNorification: React.FC<{
divRef.current.classList.add(
"opacity-0",
"transition-opacity",
"duration-300"
"duration-300",
);
setTimeout(() => {
+73 -73
View File
@@ -2,92 +2,92 @@ import { ReactNode, useEffect, useRef } from "react";
import Chess from "../../../server/src/engine";
const History: React.FC<{
chess: Chess;
buttons?: Array<{ onClick: () => void; title: string; icon: ReactNode }>;
chess: Chess;
buttons?: Array<{ onClick: () => void; title: string; icon: ReactNode }>;
}> = ({ chess, buttons }) => {
const containerRef = useRef<HTMLDivElement>(null);
const historyRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const historyRef = useRef<HTMLDivElement>(null);
const { currentPosition, history: moveHistory } = chess.getHistory();
const { currentPosition, history: moveHistory } = chess.getHistory();
useEffect(() => {
// HACK: couldn't find any other way for ChessBoard and History to have the same height
setTimeout(() => {
const chessboard = document.querySelector(
"#chessboard"
) as HTMLDivElement;
const { height } = chessboard.getBoundingClientRect();
const containerDiv = containerRef.current as HTMLDivElement;
containerDiv.style.setProperty("max-height", `${height}px`);
}, 100);
useEffect(() => {
// HACK: couldn't find any other way for ChessBoard and History to have the same height
setTimeout(() => {
const chessboard = document.querySelector(
"#chessboard",
) as HTMLDivElement;
const { height } = chessboard.getBoundingClientRect();
const containerDiv = containerRef.current as HTMLDivElement;
containerDiv.style.setProperty("max-height", `${height}px`);
}, 100);
const historyDiv = historyRef.current as HTMLDivElement;
historyDiv.scrollTop = historyDiv.scrollHeight;
}, [moveHistory.length]);
const historyDiv = historyRef.current as HTMLDivElement;
historyDiv.scrollTop = historyDiv.scrollHeight;
}, [moveHistory.length]);
const history: ReactNode[] = [];
const history: ReactNode[] = [];
moveHistory.forEach(({ san }, idx) => {
const fullMove = (idx >> 1) + 1;
const current = idx == currentPosition ? "bg-gray-500/80" : "";
moveHistory.forEach(({ san }, idx) => {
const fullMove = (idx >> 1) + 1;
const current = idx == currentPosition ? "bg-gray-500/80" : "";
if (idx % 2 == 0)
history.push(
<span key={idx} className="px-2 py-1">
{fullMove}.
</span>
);
if (idx % 2 == 0)
history.push(
<span key={idx} className="px-2 py-1">
{fullMove}.
</span>,
);
history.push(
<span key={idx + san} className="py-1">
<span className={`inline-block h-full w-2/3 rounded-md ${current}`}>
{san}
</span>
</span>
);
});
if (history.length % 3) history.push(<span key="__history-last__"></span>);
return (
<div
ref={containerRef}
className="bg-zinc-800 rounded-lg text-white hidden md:grid grid-rows-[auto,1fr,auto]"
>
<div className="text-xl text-center my-1 mx-4 border-b">History</div>
<div
ref={historyRef}
className="p-4 min-w-[13rem] h-fit max-h-full overflow-y-scroll overflow-x-hidden grid grid-cols-[auto,1fr,1fr] gap-y-2 text-center [&>:nth-child(3n)]:rounded-r-md [&>:nth-child(3n-2)]:rounded-l-md [&>:not(:nth-child(3n-2))]:font-bold [&>:nth-child(6n)]:bg-zinc-600 [&>:nth-child(6n-1)]:bg-zinc-600 [&>:nth-child(6n-2)]:bg-zinc-600"
>
{history}
</div>
<div className="bg-black grid auto-cols-fr grid-flow-col gap-5 p-4 rounded-b-lg empty:hidden">
{buttons?.map(({ onClick, title, icon }) => (
<Button key={title} title={title} onClick={onClick}>
{icon}
</Button>
))}
</div>
</div>
history.push(
<span key={idx + san} className="py-1">
<span className={`inline-block h-full w-2/3 rounded-md ${current}`}>
{san}
</span>
</span>,
);
});
if (history.length % 3) history.push(<span key="__history-last__"></span>);
return (
<div
ref={containerRef}
className="bg-zinc-800 rounded-lg text-white hidden md:grid grid-rows-[auto,1fr,auto]"
>
<div className="text-xl text-center my-1 mx-4 border-b">History</div>
<div
ref={historyRef}
className="p-4 min-w-[13rem] h-fit max-h-full overflow-y-scroll overflow-x-hidden grid grid-cols-[auto,1fr,1fr] gap-y-2 text-center [&>:nth-child(3n)]:rounded-r-md [&>:nth-child(3n-2)]:rounded-l-md [&>:not(:nth-child(3n-2))]:font-bold [&>:nth-child(6n)]:bg-zinc-600 [&>:nth-child(6n-1)]:bg-zinc-600 [&>:nth-child(6n-2)]:bg-zinc-600"
>
{history}
</div>
<div className="bg-black grid auto-cols-fr grid-flow-col gap-5 p-4 rounded-b-lg empty:hidden">
{buttons?.map(({ onClick, title, icon }) => (
<Button key={title} title={title} onClick={onClick}>
{icon}
</Button>
))}
</div>
</div>
);
};
const Button: React.FC<{
children: React.ReactNode;
title: string;
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
title: string;
disabled?: boolean;
onClick: () => void;
}> = ({ children, title, disabled, onClick }) => (
<button
className="relative min-w-[1rem] mt-auto bg-zinc-800 flex justify-center items-center p-2 rounded-md hover:bg-zinc-900 transition-colors group capitalize"
onClick={onClick}
disabled={disabled}
>
<span className="absolute -top-full left-1/2 -translate-x-1/2 bg-black rounded-md py-0.5 px-2 whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity delay-100">
{title}
</span>
{children}
</button>
<button
className="relative min-w-[1rem] mt-auto bg-zinc-800 flex justify-center items-center p-2 rounded-md hover:bg-zinc-900 transition-colors group capitalize"
onClick={onClick}
disabled={disabled}
>
<span className="absolute -top-full left-1/2 -translate-x-1/2 bg-black rounded-md py-0.5 px-2 whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity delay-100">
{title}
</span>
{children}
</button>
);
export default History;
+30 -30
View File
@@ -1,43 +1,43 @@
import { useState, useCallback } from "react";
type State = {
error: Error | null;
text: string | null;
error: Error | null;
text: string | null;
};
export default function useCopyToClipboard(): [
State,
(value: any) => Promise<void>
State,
(value: any) => Promise<void>,
] {
const [state, setState] = useState<State>({
error: null,
const [state, setState] = useState<State>({
error: null,
text: null,
});
const copyToClipboard = useCallback(async (value: string) => {
if (!navigator?.clipboard) {
return setState({
error: new Error("Clipboard not supported"),
text: null,
});
});
}
const copyToClipboard = useCallback(async (value: string) => {
if (!navigator?.clipboard) {
return setState({
error: new Error("Clipboard not supported"),
text: null,
});
}
const handleSuccess = () => {
setState({
error: null,
text: value,
});
};
const handleSuccess = () => {
setState({
error: null,
text: value,
});
};
const handleFailure = (e: any) => {
setState({
error: e,
text: null,
});
};
const handleFailure = (e: any) => {
setState({
error: e,
text: null,
});
};
navigator.clipboard.writeText(value).then(handleSuccess, handleFailure);
}, []);
navigator.clipboard.writeText(value).then(handleSuccess, handleFailure);
}, []);
return [state, copyToClipboard];
return [state, copyToClipboard];
}
+1 -1
View File
@@ -45,5 +45,5 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<RouterProvider router={router} />
</div>
</div>
</>
</>,
);
+56 -56
View File
@@ -3,71 +3,71 @@ import { useNavigate } from "react-router-dom";
import Chess, { Move } from "../../../server/src/engine";
import ChessUI from "../components/ChessUI";
import {
ArrowLeft,
CaretLeft,
CaretRight,
Plus,
Repeat,
ArrowLeft,
CaretLeft,
CaretRight,
Plus,
Repeat,
} from "../components/icons";
import InferProps from "../utils/InferProps";
const LocalGame = () => {
const [chess, setChess] = useState(Chess.load());
const [blackPerspective, setBlackPerspective] = useState(false);
const [, aux] = useState(false);
const navigate = useNavigate();
const [chess, setChess] = useState(Chess.load());
const [blackPerspective, setBlackPerspective] = useState(false);
const [, aux] = useState(false);
const navigate = useNavigate();
const rerender = () => aux((prev) => !prev);
const rerender = () => aux((prev) => !prev);
const makeMove = (move: Move) => {
if (chess.didUndo()) chess.undoMoves();
const makeMove = (move: Move) => {
if (chess.didUndo()) chess.undoMoves();
chess.makeMove(move);
chess.makeMove(move);
rerender();
};
const buttons: Pick<InferProps<[typeof ChessUI]>, "buttons">["buttons"] = [
{
onClick: () => navigate("/"),
title: "to main page",
icon: <ArrowLeft />,
},
{
onClick: () => setChess(Chess.load()),
title: "new game",
icon: <Plus />,
},
{
onClick: () => setBlackPerspective((prev) => !prev),
title: "switch sides",
icon: <Repeat />,
},
{
onClick: () => {
chess.undo();
rerender();
};
},
title: "undo",
icon: <CaretLeft />,
},
{
onClick: () => {
chess.redo();
rerender();
},
title: "redo",
icon: <CaretRight />,
},
];
const buttons: Pick<InferProps<[typeof ChessUI]>, "buttons">["buttons"] = [
{
onClick: () => navigate("/"),
title: "to main page",
icon: <ArrowLeft />,
},
{
onClick: () => setChess(Chess.load()),
title: "new game",
icon: <Plus />,
},
{
onClick: () => setBlackPerspective((prev) => !prev),
title: "switch sides",
icon: <Repeat />,
},
{
onClick: () => {
chess.undo();
rerender();
},
title: "undo",
icon: <CaretLeft />,
},
{
onClick: () => {
chess.redo();
rerender();
},
title: "redo",
icon: <CaretRight />,
},
];
return (
<ChessUI
chess={chess}
makeMove={makeMove}
blackPerspective={blackPerspective}
buttons={buttons}
/>
);
return (
<ChessUI
chess={chess}
makeMove={makeMove}
blackPerspective={blackPerspective}
buttons={buttons}
/>
);
};
export default LocalGame;
+2 -2
View File
@@ -4,14 +4,14 @@ type ArgumentTypes<F extends React.FC<any>> = F extends React.FC<infer TT>
type Head<T extends Array<React.FC<any>>> = T extends [
infer TT extends React.FC<any>,
...any
...any,
]
? TT
: never;
type Tail<T extends Array<React.FC<any>>> = T extends [
any,
...infer TT extends Array<React.FC<any>>
...infer TT extends Array<React.FC<any>>,
]
? TT
: never;
+2 -2
View File
@@ -2,8 +2,8 @@ import { io } from "socket.io-client";
// "undefined" means the URL will be computed from the `window.location` object
const URL =
process.env.NODE_ENV === "production" ? undefined : "http://localhost:5000";
process.env.NODE_ENV === "production" ? undefined : "http://localhost:5000";
export const socket = io(URL as string, {
autoConnect: false,
autoConnect: false,
});
+21 -21
View File
@@ -16,15 +16,15 @@ describe("checkmate", () => {
test("Fool's Mate", () => {
expect(
Chess.load(
"rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3"
).isCheckMate()
"rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3",
).isCheckMate(),
).toBe(true);
});
test("Scholar's Mate", () => {
expect(
Chess.load(
"rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3"
).isCheckMate()
"rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3",
).isCheckMate(),
).toBe(true);
});
});
@@ -35,19 +35,19 @@ describe("stalemate", () => {
});
test("Rosen Trap", () => {
expect(Chess.load("8/8/6k1/8/8/8/5q2/7K w - - 0 1").isStalemate()).toBe(
true
true,
);
});
test("Troitsky vs. Vogt, 1896", () => {
expect(
Chess.load(
"3k4/1pp2pp1/1b4r1/pP2p3/P3P3/6Nb/5P1P/3qB1KR w - - 0 1"
).isStalemate()
"3k4/1pp2pp1/1b4r1/pP2p3/P3P3/6Nb/5P1P/3qB1KR w - - 0 1",
).isStalemate(),
).toBe(true);
});
test("Lukany vs. Smulyan, 1938", () => {
expect(
Chess.load("8/8/8/p1K5/k1P1P3/PpP5/1P6/8 b - - 0 1").isStalemate()
Chess.load("8/8/8/p1K5/k1P1P3/PpP5/1P6/8 b - - 0 1").isStalemate(),
).toBe(true);
});
});
@@ -82,7 +82,7 @@ describe("threefold repetition", () => {
{ to: "e8", from: "e7" },
{ to: "e2", from: "e1" },
{ to: "e7", from: "e8" },
]
],
);
expect(chess.isThreefoldRepetition()).toBe(true);
});
@@ -199,31 +199,31 @@ describe("50 moves", () => {
describe("insufficient material", () => {
test("2 kings", () => {
expect(
Chess.load("8/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(true);
});
test("king and bishop", () => {
expect(
Chess.load("5b2/k7/8/6K1/8/2B5/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("5b2/k7/8/6K1/8/2B5/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(true);
});
test("king and knight", () => {
expect(
Chess.load("8/k7/8/3n2K1/8/5N2/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k7/8/3n2K1/8/5N2/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(true);
});
test("king and knight vs king and bishop", () => {
expect(
Chess.load("8/k3n3/8/6K1/8/2B5/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k3n3/8/6K1/8/2B5/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(true);
});
test("king and 2 knights", () => {
expect(
Chess.load("8/k7/4N3/6K1/8/5N2/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k7/4N3/6K1/8/5N2/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(true);
});
@@ -233,39 +233,39 @@ describe("insufficient material", () => {
test("king and 2 bishops vs king", () => {
expect(
Chess.load("8/k7/8/4B1K1/4B3/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k7/8/4B1K1/4B3/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(false);
});
test("king and 2 knights vs king and bishop", () => {
expect(
Chess.load("8/k7/3n4/6K1/2n1B3/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("8/k7/3n4/6K1/2n1B3/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(false);
});
test("king and 2 knights vs king and knight", () => {
expect(
Chess.load("6N1/k7/3n4/6K1/2n5/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("6N1/k7/3n4/6K1/2n5/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(false);
});
test("king and rook vs king", () => {
expect(
Chess.load("5r2/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("5r2/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(false);
});
test("king and queen vs king", () => {
expect(
Chess.load("5Q2/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial()
Chess.load("5Q2/k7/8/6K1/8/8/8/8 w - - 0 1").isInsufficientMaterial(),
).toBe(false);
});
test("only pawns remaining", () => {
expect(
Chess.load(
"1k6/3p4/1p6/6K1/5P2/4P3/8/8 w - - 0 1"
).isInsufficientMaterial()
"1k6/3p4/1p6/6K1/5P2/4P3/8/8 w - - 0 1",
).isInsufficientMaterial(),
).toBe(false);
});
});
+9 -9
View File
@@ -44,17 +44,17 @@ function runTest(fen: string, expectedMoves: ExpectedMoves, color?: Color) {
expect(moves).toHaveLength(expectedMoves.length);
moves.forEach((move) => expect(expectedMoves).toContainEqual(move));
expectedMoves.forEach((expectedMove) =>
expect(moves).toContainEqual(expectedMove)
expect(moves).toContainEqual(expectedMove),
);
})
)
}),
),
);
}
function runTests(
fen: string,
expectedMovesW: ExpectedMoves,
expectedMovesB: ExpectedMoves
expectedMovesB: ExpectedMoves,
) {
runTest(fen.replace("%", "w"), expectedMovesW, COLOR.WHITE);
runTest(fen.replace("%", "b"), expectedMovesB, COLOR.BLACK);
@@ -205,7 +205,7 @@ describe("pawn moves", () => {
runTests(
"r6k/1Pp1p2p/2P2p2/4p3/3P4/2P5/P5p1/K6R % - - 0 1",
expectedMovesW,
expectedMovesB
expectedMovesB,
);
});
@@ -299,7 +299,7 @@ describe("knight moves", () => {
runTests(
"n7/Kn6/8/3kN3/8/2n5/N5N1/7N % - - 0 1",
expectedMovesW,
expectedMovesB
expectedMovesB,
);
});
@@ -415,7 +415,7 @@ describe("bishop moves", () => {
runTests(
"K6b/8/3b4/2b3b1/8/8/B1BB4/7k % - - 0 1",
expectedMovesW,
expectedMovesB
expectedMovesB,
);
});
@@ -580,7 +580,7 @@ describe("en passant", () => {
runTest(
"rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 3",
expectedMoves
expectedMoves,
);
});
@@ -600,6 +600,6 @@ describe("castling", () => {
runTest(
"rn1qkbnr/p4ppp/B1p5/1p1pp3/3PPBb1/2NQ3N/PPP2PPP/R3K2R w KQkq - 0 8",
expectedMoves
expectedMoves,
);
});
+8 -8
View File
@@ -89,7 +89,7 @@ describe("valid FEN strings", () => {
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
);
for (let i = 0; i < 64; i++)
@@ -137,7 +137,7 @@ describe("valid FEN strings", () => {
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2"
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
);
for (let i = 0; i < 64; i++)
@@ -185,7 +185,7 @@ describe("valid FEN strings", () => {
const expected = builder.build();
const chess = Chess.load(
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 1 2"
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 1 2",
);
for (let i = 0; i < 64; i++)
@@ -221,7 +221,7 @@ describe("valid FEN strings", () => {
const expected = builder.build();
const chess = Chess.load(
"r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 19"
"r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 19",
);
for (let i = 0; i < 64; i++)
@@ -277,25 +277,25 @@ describe("valid FEN strings", () => {
describe("invalid FEN strings", () => {
test("string doesn't contain 6 fields", () => {
expect(() => Chess.load("8/8/2k2Q2/8/5P2/8/1p6/6K1 b - 1 48")).toThrowError(
/^Invalid FEN - string must contain 6 space delimited fields$/
/^Invalid FEN - string must contain 6 space delimited fields$/,
);
});
test("missing white king", () => {
expect(() => Chess.load("8/8/2k2Q2/8/5P2/8/1p6/8 b - - 1 48")).toThrowError(
/^Invalid FEN - board position is missing white king$/
/^Invalid FEN - board position is missing white king$/,
);
});
test("invalid turn", () => {
expect(() =>
Chess.load("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 a - - 3 43")
Chess.load("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 a - - 3 43"),
).toThrowError(/^Invalid FEN - invalid side to move$/);
});
test("invalid castling rights", () => {
expect(() =>
Chess.load("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b abc - 3 43")
Chess.load("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b abc - 3 43"),
).toThrowError(/^Invalid FEN - string contains invalid castling rights$/);
});
});
+41 -39
View File
@@ -8,33 +8,35 @@ describe("valid FEN strings", () => {
test("1. e4", () => {
expect(
validateFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
validateFEN(
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
),
).toBeUndefined();
});
test("1. e4 2. e5", () => {
expect(
validateFEN(
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2"
)
"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
),
).toBeUndefined();
});
test("1. e4 2. e5 3. ke2", () => {
expect(
validateFEN("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 1 2")
validateFEN("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPPKPPP/RNBQ1BNR b kq - 1 2"),
).toBeUndefined();
});
test("random position 1", () => {
expect(
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 19"),
).toBeUndefined();
});
test("random position 2", () => {
expect(
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43")
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43"),
).toBeUndefined();
});
@@ -46,70 +48,70 @@ describe("valid FEN strings", () => {
describe("invalid FEN strings", () => {
test("string doesn't contain 6 fields", () => {
expect(() =>
validateFEN("8/8/2k2Q2/8/5P2/8/1p6/6K1 b - 1 48")
validateFEN("8/8/2k2Q2/8/5P2/8/1p6/6K1 b - 1 48"),
).toThrowError(
/^Invalid FEN - string must contain 6 space delimited fields$/
/^Invalid FEN - string must contain 6 space delimited fields$/,
);
});
describe("invalid board position", () => {
test("doesn't contain 8 fields/rows", () => {
expect(() =>
validateFEN("8/8/2k2Q2/8/5P2/8/1p6 b - - 1 48")
validateFEN("8/8/2k2Q2/8/5P2/8/1p6 b - - 1 48"),
).toThrowError(
/^Invalid FEN - board position must contain 8 rows delimited by '\/'$/
/^Invalid FEN - board position must contain 8 rows delimited by '\/'$/,
);
});
test("missing white king", () => {
expect(() =>
validateFEN("8/8/2k2Q2/8/5P2/8/1p6/8 b - - 1 48")
validateFEN("8/8/2k2Q2/8/5P2/8/1p6/8 b - - 1 48"),
).toThrowError(/^Invalid FEN - board position is missing white king$/);
});
test("missing black king", () => {
expect(() =>
validateFEN("8/8/2K2Q2/8/5P2/8/1p6/8 b - - 1 48")
validateFEN("8/8/2K2Q2/8/5P2/8/1p6/8 b - - 1 48"),
).toThrowError(/^Invalid FEN - board position is missing black king$/);
});
test("too many white kings", () => {
expect(() =>
validateFEN("8/7K/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48")
validateFEN("8/7K/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48"),
).toThrowError(
/^Invalid FEN - board position contains too many white kings$/
/^Invalid FEN - board position contains too many white kings$/,
);
});
test("too many black kings", () => {
expect(() =>
validateFEN("8/7k/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48")
validateFEN("8/7k/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48"),
).toThrowError(
/^Invalid FEN - board position contains too many black kings$/
/^Invalid FEN - board position contains too many black kings$/,
);
});
test("consecutive digits", () => {
expect(() =>
validateFEN("8/62/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48")
validateFEN("8/62/2k2Q2/8/5P2/8/1p6/6K1 b - - 1 48"),
).toThrowError(
/^Invalid FEN - board position contains consecutive digits$/
/^Invalid FEN - board position contains consecutive digits$/,
);
});
test("invalid piece", () => {
expect(() =>
validateFEN("1k6/1Gp5/p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43")
validateFEN("1k6/1Gp5/p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43"),
).toThrowError(
/^Invalid FEN - board position contains an invalid piece symbol: G$/
/^Invalid FEN - board position contains an invalid piece symbol: G$/,
);
});
test("too many squares on row", () => {
expect(() =>
validateFEN("1k6/1pp5/1p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43")
validateFEN("1k6/1pp5/1p7/5B1p/PP6/6K1/4p2r/4R3 b - - 3 43"),
).toThrowError(
/^Invalid FEN - board position contains a row that does not have 8 squares$/
/^Invalid FEN - board position contains a row that does not have 8 squares$/,
);
});
});
@@ -117,19 +119,19 @@ describe("invalid FEN strings", () => {
describe("invalid turn", () => {
test("capital W", () => {
expect(() =>
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 W - - 3 43")
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 W - - 3 43"),
).toThrowError(/^Invalid FEN - invalid side to move$/);
});
test("capital B", () => {
expect(() =>
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 B - - 3 43")
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 B - - 3 43"),
).toThrowError(/^Invalid FEN - invalid side to move$/);
});
test("invalid letter", () => {
expect(() =>
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 c - - 3 43")
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 c - - 3 43"),
).toThrowError(/^Invalid FEN - invalid side to move$/);
});
});
@@ -137,7 +139,7 @@ describe("invalid FEN strings", () => {
describe("invalid castling rights", () => {
test("invalid characters", () => {
expect(() =>
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b abc - 3 43")
validateFEN("1k6/1pp5/p7/5B1p/PP6/6K1/4p2r/4R3 b abc - 3 43"),
).toThrowError(/^Invalid FEN - string contains invalid castling rights$/);
});
});
@@ -145,19 +147,19 @@ describe("invalid FEN strings", () => {
describe("invalid en-passant", () => {
test("random position 1", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - e1 0 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - e1 0 19"),
).toThrowError(/^Invalid FEN - invalid en-passant square$/);
});
test("random position 1", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 w - e3 0 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 w - e3 0 19"),
).toThrowError(/^Invalid FEN - invalid en-passant square$/);
});
test("random position 1", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - e6 0 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - e6 0 19"),
).toThrowError(/^Invalid FEN - invalid en-passant square$/);
});
});
@@ -165,17 +167,17 @@ describe("invalid FEN strings", () => {
describe("invalid half moves", () => {
test("negative number", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - -2 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - -2 19"),
).toThrowError(
/^Invalid FEN - move number must be a non-negative integer$/
/^Invalid FEN - move number must be a non-negative integer$/,
);
});
test("not a number", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - abc 19")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - abc 19"),
).toThrowError(
/^Invalid FEN - move number must be a non-negative integer$/
/^Invalid FEN - move number must be a non-negative integer$/,
);
});
});
@@ -183,25 +185,25 @@ describe("invalid FEN strings", () => {
describe("invalid full moves", () => {
test("negative number", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 -3")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 -3"),
).toThrowError(
/^Invalid FEN - number of full moves must be a positive integer$/
/^Invalid FEN - number of full moves must be a positive integer$/,
);
});
test("zero", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 0")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 0"),
).toThrowError(
/^Invalid FEN - number of full moves must be a positive integer$/
/^Invalid FEN - number of full moves must be a positive integer$/,
);
});
test("not a number", () => {
expect(() =>
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 abc")
validateFEN("r1b1r1k1/pp4pp/3Bpp2/8/2q5/P5Q1/3R1PPP/R5K1 b - - 0 abc"),
).toThrowError(
/^Invalid FEN - number of full moves must be a positive integer$/
/^Invalid FEN - number of full moves must be a positive integer$/,
);
});
});
+20 -19
View File
@@ -145,7 +145,7 @@ export function validateFEN(fen: string): void {
if (fields.length != 6)
throw new Error(
"Invalid FEN - string must contain 6 space delimited fields"
"Invalid FEN - string must contain 6 space delimited fields",
);
const validatePosition = (position: string) => {
@@ -153,7 +153,7 @@ export function validateFEN(fen: string): void {
if (rows.length != 8)
throw new Error(
"Invalid FEN - board position must contain 8 rows delimited by '/'"
"Invalid FEN - board position must contain 8 rows delimited by '/'",
);
const kings = [
@@ -166,12 +166,12 @@ export function validateFEN(fen: string): void {
if (matches.length == 0)
throw new Error(
`Invalid FEN - board position is missing ${king.color} king`
`Invalid FEN - board position is missing ${king.color} king`,
);
if (matches.length > 1)
throw new Error(
`Invalid FEN - board position contains too many ${king.color} kings`
`Invalid FEN - board position contains too many ${king.color} kings`,
);
}
@@ -183,7 +183,7 @@ export function validateFEN(fen: string): void {
if (isDigit(symbol)) {
if (previousWasNumber)
throw new Error(
"Invalid FEN - board position contains consecutive digits"
"Invalid FEN - board position contains consecutive digits",
);
numSquares += parseInt(symbol);
@@ -193,12 +193,12 @@ export function validateFEN(fen: string): void {
if (
!(Object.values(PIECE) as Array<string>).includes(
symbol.toLowerCase()
symbol.toLowerCase(),
)
)
throw new Error(
"Invalid FEN - board position contains an invalid piece symbol: " +
symbol
symbol,
);
numSquares++;
@@ -207,7 +207,7 @@ export function validateFEN(fen: string): void {
if (numSquares != 8)
throw new Error(
"Invalid FEN - board position contains a row that does not have 8 squares"
"Invalid FEN - board position contains a row that does not have 8 squares",
);
});
};
@@ -233,14 +233,14 @@ export function validateFEN(fen: string): void {
const validateHalfMoves = (halfMoves: string) => {
if (/^\d+$/.test(halfMoves) == false)
throw new Error(
"Invalid FEN - move number must be a non-negative integer"
"Invalid FEN - move number must be a non-negative integer",
);
};
const validateFullMoves = (fullMoves: string) => {
if (/^[1-9]\d*$/.test(fullMoves) == false)
throw new Error(
"Invalid FEN - number of full moves must be a positive integer"
"Invalid FEN - number of full moves must be a positive integer",
);
};
@@ -326,7 +326,7 @@ function generatePawnMoves(
position: number,
color: Color,
board: Readonly<Board>,
enPassantSquare: Square
enPassantSquare: Square,
): Array<InternalMove> {
if (board[position] == null) return [];
@@ -343,7 +343,7 @@ function generatePawnMoves(
to: toAlgebraic,
promotion: piece,
flags: MOVE_FLAGS.PROMOTION | (flag ?? 0),
})
}),
);
};
@@ -404,7 +404,7 @@ function generatePieceMoves(
position: number,
piece: Piece,
board: Readonly<Board>,
enPassantSquare: Square
enPassantSquare: Square,
): Array<InternalMove> {
if (piece.type == PIECE.PAWN)
return generatePawnMoves(position, piece.color, board, enPassantSquare);
@@ -524,7 +524,7 @@ export default class Chess {
fullMoves: number,
kings: Record<Color, number>,
enableProcessMoves: boolean,
enableHistory: boolean
enableHistory: boolean,
) {
this._board = board;
this._turn = turn;
@@ -549,7 +549,8 @@ export default class Chess {
if (this._turn == piece.color) this._moves = this._moves.concat(moves);
moves.forEach(
({ to }) => (this._attacks[squareIndex(to)] |= COLOR_MASKS[piece.color])
({ to }) =>
(this._attacks[squareIndex(to)] |= COLOR_MASKS[piece.color]),
);
}
@@ -629,7 +630,7 @@ export default class Chess {
{
enableProcessMoves,
enableHistory,
}: { enableProcessMoves: boolean; enableHistory: boolean }
}: { enableProcessMoves: boolean; enableHistory: boolean },
) {
validateFEN(fen);
@@ -777,7 +778,7 @@ export default class Chess {
switch (move.flags) {
case MOVE_FLAGS.PAWN_JUMP:
this._enPassant = algebraic(
squareIndex(move.to) - PAWN_MOVE_INFO[myColor].offset
squareIndex(move.to) - PAWN_MOVE_INFO[myColor].offset,
);
keepEpSquare = true;
break;
@@ -1054,7 +1055,7 @@ export default class Chess {
chess._undoCount = 0;
chess._history = this._history.slice(
0,
this._history.length - this._undoCount
this._history.length - this._undoCount,
);
return chess;
@@ -1154,7 +1155,7 @@ export default class Chess {
this._fullMoves,
this._kings,
this._enableProcessMoves,
this._enableHistory
this._enableHistory,
);
}
};
+4 -4
View File
@@ -26,7 +26,7 @@ const io = new Server(
origin: "http://localhost:3000",
},
}
: {}
: {},
);
type Room = {
@@ -105,7 +105,7 @@ if (process.env.NODE_ENV === "development")
w: room.w,
b: room.b,
game: room.game.getFEN(),
})
}),
);
return res.json(Object.fromEntries(m));
});
@@ -140,7 +140,7 @@ if (process.env.NODE_ENV === "production") {
app.use(express.static(path.join(__dirname, "client", "dist")));
app.get("*", (_req, res) =>
res.sendFile(path.resolve(__dirname, "client", "dist", "index.html"))
res.sendFile(path.resolve(__dirname, "client", "dist", "index.html")),
);
} else app.get("/", (_req, res) => res.send("Hello, world!"));
@@ -161,5 +161,5 @@ app.use((err: Error, _req: Request, res: Response) => {
});
server.listen(PORT, () =>
console.log("Server listening on http://localhost:" + PORT)
console.log("Server listening on http://localhost:" + PORT),
);