Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ const nextConfig = (phase: string): NextConfig => ({
},
],
},
{
source: "/playground",
headers: [
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
],
},
{
source: "/database",
headers: [
Expand Down
85 changes: 85 additions & 0 deletions src/pages/playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { PageTitle } from "@/components/pageTitle";
import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage";
import { useEngine } from "@/hooks/useEngine";
import { Stockfish16_1 } from "@/lib/engine/stockfish16_1";
import { isEngineSupported } from "@/lib/engine/shared";
import PlaygroundBoard from "@/sections/playground/board";
import PlaygroundEngineLines from "@/sections/playground/engineLines";
import { usePlaygroundCurrentPosition } from "@/sections/playground/hooks/useCurrentPosition";
import PlaygroundToolbar from "@/sections/playground/toolbar";
import { engineNameAtom } from "@/sections/analysis/states";
import { DEFAULT_ENGINE, ENGINE_LABELS } from "@/constants";
import { EngineName } from "@/types/enums";
import { Grid2 as Grid, Typography } from "@mui/material";
import { useEffect } from "react";

export default function Playground() {
const [engineName, setEngineName] = useAtomLocalStorage(
"engine-name",
engineNameAtom
);
const engine = useEngine(engineName);

useEffect(() => {
if (!isEngineSupported(engineName)) {
if (Stockfish16_1.isSupported()) {
setEngineName(EngineName.Stockfish16_1Lite);
} else {
setEngineName(EngineName.Stockfish11);
}
}
}, [engineName, setEngineName]);

usePlaygroundCurrentPosition(engine);

return (
<Grid container gap={4} justifyContent="space-evenly" alignItems="start">
<PageTitle title="Chesskit Playground" />

<PlaygroundBoard />

<Grid
container
marginTop={{ xs: 0, md: "2.5em" }}
justifyContent="center"
alignItems="center"
borderRadius={2}
border={1}
borderColor={"secondary.main"}
size={{
xs: 12,
md: "grow",
}}
sx={{
backgroundColor: "secondary.main",
borderColor: "primary.main",
borderWidth: 2,
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.5)",
}}
padding={3}
rowGap={3}
style={{
maxWidth: "430px",
}}
>
<Grid container size={12} justifyContent="center" rowGap={1}>
<Typography variant="h5" align="center">
Playground
</Typography>
<Typography variant="body2" align="center" color="text.secondary">
Free play with live analysis from{" "}
{ENGINE_LABELS[engineName ?? DEFAULT_ENGINE].small}
</Typography>
</Grid>

<Grid container size={12} justifyContent="center">
<PlaygroundToolbar />
</Grid>

<Grid container size={12}>
<PlaygroundEngineLines />
</Grid>
</Grid>
</Grid>
);
}
5 changes: 5 additions & 0 deletions src/sections/layout/NavMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {

const MenuOptions = [
{ text: "Play", icon: "streamline:chess-pawn", href: "/play" },
{
text: "Playground",
icon: "mdi:flask-outline",
href: "/playground",
},
{ text: "Analysis", icon: "streamline:magnifying-glass-solid", href: "/" },
{
text: "Database",
Expand Down
43 changes: 43 additions & 0 deletions src/sections/playground/board.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Board from "@/components/board";
import { usePlayersData } from "@/hooks/usePlayersData";
import { useScreenSize } from "@/hooks/useScreenSize";
import { showPlayerMoveIconAtom } from "@/sections/analysis/states";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
import {
playgroundBoardOrientationAtom,
playgroundCurrentPositionAtom,
playgroundGameAtom,
} from "./states";

export default function PlaygroundBoard() {
const screenSize = useScreenSize();
const boardOrientation = useAtomValue(playgroundBoardOrientationAtom);
const { white, black } = usePlayersData(playgroundGameAtom);

const boardSize = useMemo(() => {
const width = screenSize.width;
const height = screenSize.height;

if (window?.innerWidth < 1200) {
return Math.min(width, height - 150);
}

return Math.min(width - 700, height * 0.92);
}, [screenSize]);

return (
<Board
id="PlaygroundBoard"
boardSize={boardSize}
canPlay={true}
gameAtom={playgroundGameAtom}
whitePlayer={white}
blackPlayer={black}
boardOrientation={boardOrientation}
currentPositionAtom={playgroundCurrentPositionAtom}
showPlayerMoveIconAtom={showPlayerMoveIconAtom}
showEvaluationBar={true}
/>
);
}
123 changes: 123 additions & 0 deletions src/sections/playground/engineLines.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import PrettyMoveSan from "@/components/prettyMoveSan";
import { useChessActions } from "@/hooks/useChessActions";
import { getLineEvalLabel, moveLineUciToSan } from "@/lib/chess";
import { LineEval } from "@/types/eval";
import { List, ListItem, Skeleton, Stack, Typography } from "@mui/material";
import { useAtomValue } from "jotai";
import { playgroundCurrentPositionAtom, playgroundGameAtom } from "./states";

const linesSkeleton: LineEval[] = Array.from({ length: 3 }).map((_, i) => ({
pv: [`${i}`],
depth: 0,
multiPv: i + 1,
}));

export default function PlaygroundEngineLines() {
const game = useAtomValue(playgroundGameAtom);
const position = useAtomValue(playgroundCurrentPositionAtom);
const { addMoves } = useChessActions(playgroundGameAtom);

const lines = position.eval?.lines?.length
? position.eval.lines.slice(0, 3)
: linesSkeleton;
const uciToSan = moveLineUciToSan(game.fen());
const turn = game.turn();

const getColorFromMoveIdx = (moveIdx: number): "w" | "b" => {
if (moveIdx % 2 === 0) return turn;
return turn === "w" ? "b" : "w";
};

return (
<Stack rowGap={2} width="100%">
<Stack rowGap={0.5} alignItems="center">
<Typography variant="h6">Stockfish top lines</Typography>
{position.opening && (
<Typography variant="body2" color="text.secondary" textAlign="center">
{position.opening}
</Typography>
)}
</Stack>

{game.isGameOver() && (
<Typography align="center" fontSize="0.9rem">
Game is over
</Typography>
)}

<List sx={{ width: "100%", padding: 0 }}>
{lines.map((line) => {
const showSkeleton = line.depth < 6;
const isBlackEval =
(line.cp !== undefined && line.cp < 0) ||
(line.mate !== undefined && line.mate < 0);

return (
<ListItem
key={line.multiPv}
disablePadding
sx={{ marginBottom: 1 }}
>
<Typography
marginRight={1.5}
marginY={0.3}
paddingY={0.2}
noWrap
overflow="visible"
width="3.5em"
minWidth="3.5em"
textAlign="center"
fontSize="0.8rem"
sx={{
backgroundColor: isBlackEval ? "black" : "white",
color: isBlackEval ? "white" : "black",
}}
borderRadius="5px"
border="1px solid #424242"
fontWeight="500"
>
{showSkeleton ? (
<Skeleton
variant="rounded"
animation="wave"
sx={{ color: "transparent" }}
>
placeholder
</Skeleton>
) : (
getLineEvalLabel(line)
)}
</Typography>

<Typography noWrap fontSize="0.9rem">
{showSkeleton ? (
<Skeleton variant="rounded" animation="wave" width="20em" />
) : (
line.pv.map((uci, i) => (
<PrettyMoveSan
key={`${line.multiPv}-${uci}-${i}`}
san={uciToSan(uci)}
color={getColorFromMoveIdx(i)}
additionalText={i < line.pv.length - 1 ? "," : ""}
boxProps={{
onClick: () => addMoves(line.pv.slice(0, i + 1)),
sx: {
cursor: "pointer",
ml: i ? 0.5 : 0,
transition: "opacity 0.2s ease-in-out",
"&:hover": {
opacity: 0.5,
},
},
}}
/>
))
)}
</Typography>
</ListItem>
);
})}
</List>
</Stack>
);
}
Loading