Squashed commit of the following:

commit dfc79cf287823383a25a650d5788ee5250b1c316
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date:   Sun May 11 01:32:35 2025 +0200

    fix : style

commit bccfa5a3358302c2f037cc2dcfbd0a1df5e2974e
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date:   Sun May 11 01:01:12 2025 +0200

    feat : players clocks v1

commit 5f65009f200686433904710d5f9ceb1ba166fa9d
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date:   Sat May 10 21:58:02 2025 +0200

    fix : merge issues

commit f93dc6104e2d3fbb60088f578c2d1f13bf6519e9
Merge: a9f3728 fea1f3f
Author: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com>
Date:   Sat May 10 21:53:11 2025 +0200

    Merge branch 'main' into feat/add-players-clocks

commit a9f372808ef403dfb823c4cf93c837412cc55c53
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date:   Mon Jan 6 23:10:28 2025 +0100

    fix : rename

commit aedf9c252023bebe4da4327b7526371fa75b7b3e
Author: GuillaumeSD <gsd.lfny@gmail.com>
Date:   Sun Jan 5 17:30:27 2025 +0100

    feat : add players clocks
This commit is contained in:
GuillaumeSD
2025-05-11 01:33:10 +02:00
parent fea1f3fe47
commit 74a2adbb7d
17 changed files with 169 additions and 57 deletions

View File

@@ -8,7 +8,7 @@ export interface Props {
color: Color;
}
const PIECE_SCALE = 0.6;
const PIECE_SCALE = 0.55;
export default function CapturedPieces({ fen, color }: Props) {
const cssProps = useMemo(() => {
@@ -22,7 +22,7 @@ export default function CapturedPieces({ fen, color }: Props) {
}, [fen, color]);
return (
<Grid container alignItems="end" columnGap={0.6} size="auto">
<Grid container alignItems="end" columnGap={0.5} size="auto">
{cssProps.map((cssProp, i) => (
<span
key={i}

View File

@@ -270,7 +270,7 @@ export default function Board({
<Grid
container
rowGap={1}
rowGap={1.5}
justifyContent="center"
alignItems="center"
paddingLeft={showEvaluationBar ? 2 : 0}
@@ -278,7 +278,7 @@ export default function Board({
>
<PlayerHeader
color={boardOrientation === Color.White ? Color.Black : Color.White}
fen={gameFen}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? blackPlayer : whitePlayer}
/>
@@ -318,7 +318,7 @@ export default function Board({
<PlayerHeader
color={boardOrientation}
fen={gameFen}
gameAtom={gameAtom}
player={boardOrientation === Color.White ? whitePlayer : blackPlayer}
/>
</Grid>

View File

@@ -1,35 +1,113 @@
import { Color } from "@/types/enums";
import { Player } from "@/types/game";
import { Avatar, Grid2 as Grid, Typography } from "@mui/material";
import { Avatar, Grid2 as Grid, Stack, Typography } from "@mui/material";
import CapturedPieces from "./capturedPieces";
import { PrimitiveAtom, useAtomValue } from "jotai";
import { Chess } from "chess.js";
import { useMemo } from "react";
import { getPaddedNumber } from "@/lib/helpers";
export interface Props {
player: Player;
color: Color;
fen: string;
gameAtom: PrimitiveAtom<Chess>;
}
export default function PlayerHeader({ color, player, fen }: Props) {
export default function PlayerHeader({ color, player, gameAtom }: Props) {
const game = useAtomValue(gameAtom);
const gameFen = game.fen();
const clock = useMemo(() => {
const turn = game.turn();
if (turn === color) {
const history = game.history({ verbose: true });
const previousFen = history.at(-1)?.before;
const comment = game
.getComments()
.find(({ fen }) => fen === previousFen)?.comment;
return getClock(comment);
}
const comment = game.getComment();
return getClock(comment);
}, [game, color]);
return (
<Grid
container
justifyContent="center"
justifyContent="space-between"
alignItems="center"
columnGap={2}
size={12}
>
{player.avatarUrl && (
<Stack direction="row">
<Avatar
src={player.avatarUrl}
alt={player.name}
variant="circular"
sx={{ width: 24, height: 24 }}
/>
)}
<Typography>
{player.rating ? `${player.name} (${player.rating})` : player.name}
</Typography>
sx={{
width: 40,
height: 40,
backgroundColor: color === Color.White ? "white" : "black",
color: color === Color.White ? "black" : "white",
border: "1px solid black",
}}
>
{player.name[0].toUpperCase()}
</Avatar>
<CapturedPieces fen={fen} color={color} />
<Stack marginLeft={1}>
<Stack direction="row">
<Typography fontSize="0.9rem">{player.name}</Typography>
{player.rating && (
<Typography marginLeft={0.5} fontSize="0.9rem" fontWeight="200">
({player.rating})
</Typography>
)}
</Stack>
<CapturedPieces fen={gameFen} color={color} />
</Stack>
</Stack>
{clock && (
<Typography
align="center"
sx={{
backgroundColor: color === Color.White ? "white" : "black",
color: color === Color.White ? "black" : "white",
}}
borderRadius="5px"
padding={0.8}
border="1px solid #424242"
width="5rem"
textAlign="right"
>
{clock.hours ? `${clock.hours}:` : ""}
{getPaddedNumber(clock.minutes)}:{getPaddedNumber(clock.seconds)}
{clock.hours || clock.minutes || clock.seconds > 20
? ""
: `.${clock.tenths}`}
</Typography>
)}
</Grid>
);
}
const getClock = (comment: string | undefined) => {
if (!comment) return undefined;
const match = comment.match(/\[%clk (\d+):(\d+):(\d+)(?:\.(\d*))?\]/);
if (!match) return undefined;
return {
hours: parseInt(match[1]),
minutes: parseInt(match[2]),
seconds: parseInt(match[3]),
tenths: match[4] ? parseInt(match[4]) : 0,
};
};

View File

@@ -1,11 +1,11 @@
import { setGameHeaders } from "@/lib/chess";
import { getGameFromPgn, setGameHeaders } from "@/lib/chess";
import {
playGameEndSound,
playIllegalMoveSound,
playSoundFromMove,
} from "@/lib/sounds";
import { Player } from "@/types/game";
import { Chess, Move } from "chess.js";
import { Chess, Move, DEFAULT_POSITION } from "chess.js";
import { PrimitiveAtom, useAtom } from "jotai";
import { useCallback } from "react";
@@ -43,8 +43,9 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
if (game.history().length === 0) {
const pgnSplitted = game.pgn().split("]");
if (
pgnSplitted.at(-1)?.includes("1-0") ||
pgnSplitted.at(-1) === "\n *"
["1-0", "0-1", "1/2-1/2", "*"].includes(
pgnSplitted.at(-1)?.trim() ?? ""
)
) {
newGame.loadPgn(pgnSplitted.slice(0, -1).join("]") + "]");
return newGame;
@@ -55,11 +56,31 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
return newGame;
}, [game]);
const resetToStartingPosition = useCallback(
(pgn?: string) => {
const newGame = pgn ? getGameFromPgn(pgn) : copyGame();
newGame.load(newGame.getHeaders().FEN || DEFAULT_POSITION, {
preserveHeaders: true,
});
setGame(newGame);
},
[copyGame, setGame]
);
const makeMove = useCallback(
(move: { from: string; to: string; promotion?: string }): Move | null => {
(params: {
from: string;
to: string;
promotion?: string;
comment?: string;
}): Move | null => {
const newGame = copyGame();
try {
const { comment, ...move } = params;
const result = newGame.move(move);
if (comment) newGame.setComment(comment);
setGame(newGame);
playSoundFromMove(result);
return result;
@@ -103,5 +124,12 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
[setGame]
);
return { setPgn, reset, makeMove, undoMove, goToMove };
return {
setPgn,
reset,
makeMove,
undoMove,
goToMove,
resetToStartingPosition,
};
};

View File

@@ -280,17 +280,6 @@ const getPieceValue = (piece: PieceSymbol): number => {
}
};
export const getStartingFen = (
params: { pgn: string } | { game: Chess }
): string => {
const game = "game" in params ? params.game : getGameFromPgn(params.pgn);
const history = game.history({ verbose: true });
if (!history.length) return game.fen();
return history[0].before;
};
export const isCheck = (fen: string): boolean => {
const game = new Chess(fen);
return game.inCheck();

View File

@@ -1,5 +1,5 @@
import { ChessComGame } from "@/types/chessCom";
import { getPaddedMonth } from "./helpers";
import { getPaddedNumber } from "./helpers";
export const getChessComUserRecentGames = async (
username: string
@@ -7,7 +7,7 @@ export const getChessComUserRecentGames = async (
const date = new Date();
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const paddedMonth = getPaddedMonth(month);
const paddedMonth = getPaddedNumber(month);
const res = await fetch(
`https://api.chess.com/pub/player/${username}/games/${year}/${paddedMonth}`
@@ -21,7 +21,7 @@ export const getChessComUserRecentGames = async (
if (games.length < 50) {
const previousMonth = month === 1 ? 12 : month - 1;
const previousPaddedMonth = getPaddedMonth(previousMonth);
const previousPaddedMonth = getPaddedNumber(previousMonth);
const yearToFetch = previousMonth === 12 ? year - 1 : year;
const resPreviousMonth = await fetch(

View File

@@ -1,4 +1,4 @@
export const getPaddedMonth = (month: number) => {
export const getPaddedNumber = (month: number) => {
return month < 10 ? `0${month}` : month;
};

View File

@@ -58,7 +58,7 @@ export const getLichessUserRecentGames = async (
username: string
): Promise<LichessGame[]> => {
const res = await fetch(
`https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc`,
`https://lichess.org/api/games/user/${username}?until=${Date.now()}&max=50&pgnInJson=true&sort=dateDesc&clocks=true`,
{ method: "GET", headers: { accept: "application/x-ndjson" } }
);

View File

@@ -81,7 +81,7 @@ export default function GameReview() {
maxWidth: "1200px",
}}
rowGap={2}
maxHeight={{ lg: "calc(95vh - 130px)", xs: "900px" }}
maxHeight={{ lg: "calc(95vh - 80px)", xs: "900px" }}
display="grid"
gridTemplateRows="repeat(3, auto) fit-content(100%)"
marginTop={isLgOrGreater && window.innerHeight > 780 ? 4 : 0}

View File

@@ -28,7 +28,7 @@ export default function BoardContainer() {
return Math.min(width, height - 150);
}
return Math.min(width - 700, height * 0.95);
return Math.min(width - 700, height * 0.92);
}, [screenSize]);
return (

View File

@@ -42,9 +42,20 @@ export const useCurrentPosition = (engineName?: EngineName) => {
if (gameEval) {
const evalIndex = boardHistory.length;
position.eval = gameEval.positions[evalIndex];
position.eval = {
...gameEval.positions[evalIndex],
lines: gameEval.positions[evalIndex].lines.slice(0, multiPv),
};
position.lastEval =
evalIndex > 0 ? gameEval.positions[evalIndex - 1] : undefined;
evalIndex > 0
? {
...gameEval.positions[evalIndex - 1],
lines: gameEval.positions[evalIndex - 1].lines.slice(
0,
multiPv
),
}
: undefined;
}
}
@@ -70,11 +81,12 @@ export const useCurrentPosition = (engineName?: EngineName) => {
(savedEval.lines?.length ?? 0) >= multiPv &&
(savedEval.lines[0].depth ?? 0) >= depth
) {
setPartialEval?.({
const positionEval: PositionEval = {
...savedEval,
lines: savedEval.lines.slice(0, multiPv),
});
return savedEval;
};
setPartialEval?.(positionEval);
return positionEval;
}
const rawPositionEval = await engine.evaluatePositionWithUpdate({

View File

@@ -12,7 +12,10 @@ export default function Opening() {
}
if (!lastMove) return null;
const opening = position?.eval?.opening || lastOpening;
const opening =
position?.eval?.opening && !position?.eval?.opening.includes("Unknown")
? position.eval.opening
: lastOpening;
if (opening && opening !== lastOpening) {
setLastOpening(opening);
}

View File

@@ -12,13 +12,12 @@ import { useGameDatabase } from "@/hooks/useGameDatabase";
import { useAtomValue, useSetAtom } from "jotai";
import { Chess } from "chess.js";
import { useRouter } from "next/router";
import { getStartingFen } from "@/lib/chess";
export default function LoadGame() {
const router = useRouter();
const game = useAtomValue(gameAtom);
const { setPgn: setGamePgn } = useChessActions(gameAtom);
const { reset: resetBoard } = useChessActions(boardAtom);
const { resetToStartingPosition: resetBoard } = useChessActions(boardAtom);
const { gameFromUrl } = useGameDatabase();
const setEval = useSetAtom(gameEvalAtom);
const setBoardOrientation = useSetAtom(boardOrientationAtom);
@@ -26,7 +25,7 @@ export default function LoadGame() {
const resetAndSetGamePgn = useCallback(
(pgn: string) => {
resetBoard({ fen: getStartingFen({ pgn }) });
resetBoard(pgn);
setEval(undefined);
setGamePgn(pgn);
},

View File

@@ -8,11 +8,10 @@ import NextMoveButton from "./nextMoveButton";
import GoToLastPositionButton from "./goToLastPositionButton";
import SaveButton from "./saveButton";
import { useEffect } from "react";
import { getStartingFen } from "@/lib/chess";
export default function PanelToolBar() {
const board = useAtomValue(boardAtom);
const { reset: resetBoard, undoMove: undoBoardMove } =
const { resetToStartingPosition: resetBoard, undoMove: undoBoardMove } =
useChessActions(boardAtom);
const boardHistory = board.history();
@@ -24,7 +23,7 @@ export default function PanelToolBar() {
if (e.key === "ArrowLeft") {
undoBoardMove();
} else if (e.key === "ArrowDown") {
resetBoard({ fen: getStartingFen({ game: board }) });
resetBoard();
}
};
@@ -42,7 +41,7 @@ export default function PanelToolBar() {
<Tooltip title="Reset board">
<Grid>
<IconButton
onClick={() => resetBoard({ fen: getStartingFen({ game: board }) })}
onClick={() => resetBoard()}
disabled={boardHistory.length === 0}
sx={{ paddingX: 1.2, paddingY: 0.5 }}
>

View File

@@ -22,12 +22,16 @@ export default function NextMoveButton() {
const nextMoveIndex = boardHistory.length;
const nextMove = game.history({ verbose: true })[nextMoveIndex];
const comment = game
.getComments()
.find((c) => c.fen === nextMove.after)?.comment;
if (nextMove) {
makeBoardMove({
from: nextMove.from,
to: nextMove.to,
promotion: nextMove.promotion,
comment,
});
}
}, [isButtonEnabled, boardHistory, game, makeBoardMove]);

View File

@@ -35,7 +35,7 @@ export default function Layout({ children }: PropsWithChildren) {
darkMode={useDarkMode}
switchDarkMode={() => setDarkMode((val) => !val)}
/>
<main style={{ margin: "2em 2vw" }}>{children}</main>
<main style={{ margin: "3vh 2vw" }}>{children}</main>
</ThemeProvider>
);
}

View File

@@ -59,7 +59,7 @@ export default function BoardContainer() {
return Math.min(width, height - 150);
}
return Math.min(width - 300, height * 0.85);
return Math.min(width - 300, height * 0.83);
}, [screenSize]);
useGameData(gameAtom, gameDataAtom);