From 74a2adbb7da5b5c27acbd788180043acf691047a Mon Sep 17 00:00:00 2001 From: GuillaumeSD <47183782+GuillaumeSD@users.noreply.github.com> Date: Sun, 11 May 2025 01:33:10 +0200 Subject: [PATCH] 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 Date: Mon Jan 6 23:10:28 2025 +0100 fix : rename commit aedf9c252023bebe4da4327b7526371fa75b7b3e Author: GuillaumeSD Date: Sun Jan 5 17:30:27 2025 +0100 feat : add players clocks --- src/components/board/capturedPieces.tsx | 4 +- src/components/board/index.tsx | 6 +- src/components/board/playerHeader.tsx | 104 +++++++++++++++--- src/hooks/useChessActions.ts | 40 ++++++- src/lib/chess.ts | 11 -- src/lib/chessCom.ts | 6 +- src/lib/helpers.ts | 2 +- src/lib/lichess.ts | 2 +- src/pages/index.tsx | 2 +- src/sections/analysis/board/index.tsx | 2 +- .../analysis/hooks/useCurrentPosition.ts | 22 +++- .../panelBody/analysisTab/opening.tsx | 5 +- .../analysis/panelHeader/loadGame.tsx | 5 +- src/sections/analysis/panelToolbar/index.tsx | 7 +- .../analysis/panelToolbar/nextMoveButton.tsx | 4 + src/sections/layout/index.tsx | 2 +- src/sections/play/board.tsx | 2 +- 17 files changed, 169 insertions(+), 57 deletions(-) diff --git a/src/components/board/capturedPieces.tsx b/src/components/board/capturedPieces.tsx index 6773389..4c612a5 100644 --- a/src/components/board/capturedPieces.tsx +++ b/src/components/board/capturedPieces.tsx @@ -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 ( - + {cssProps.map((cssProp, i) => ( @@ -318,7 +318,7 @@ export default function Board({ diff --git a/src/components/board/playerHeader.tsx b/src/components/board/playerHeader.tsx index 0b6a637..8155ed7 100644 --- a/src/components/board/playerHeader.tsx +++ b/src/components/board/playerHeader.tsx @@ -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; } -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 ( - {player.avatarUrl && ( + - )} - - {player.rating ? `${player.name} (${player.rating})` : player.name} - + 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()} + - + + + {player.name} + + {player.rating && ( + + ({player.rating}) + + )} + + + + + + + {clock && ( + + {clock.hours ? `${clock.hours}:` : ""} + {getPaddedNumber(clock.minutes)}:{getPaddedNumber(clock.seconds)} + {clock.hours || clock.minutes || clock.seconds > 20 + ? "" + : `.${clock.tenths}`} + + )} ); } + +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, + }; +}; diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index 6c8cf0f..3efdfd2 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -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) => { 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) => { 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) => { [setGame] ); - return { setPgn, reset, makeMove, undoMove, goToMove }; + return { + setPgn, + reset, + makeMove, + undoMove, + goToMove, + resetToStartingPosition, + }; }; diff --git a/src/lib/chess.ts b/src/lib/chess.ts index e8eb446..a9a9baf 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -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(); diff --git a/src/lib/chessCom.ts b/src/lib/chessCom.ts index 63eecb9..a7176d9 100644 --- a/src/lib/chessCom.ts +++ b/src/lib/chessCom.ts @@ -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( diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index af7f49a..39d59c8 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,4 +1,4 @@ -export const getPaddedMonth = (month: number) => { +export const getPaddedNumber = (month: number) => { return month < 10 ? `0${month}` : month; }; diff --git a/src/lib/lichess.ts b/src/lib/lichess.ts index 389a319..cef22d6 100644 --- a/src/lib/lichess.ts +++ b/src/lib/lichess.ts @@ -58,7 +58,7 @@ export const getLichessUserRecentGames = async ( username: string ): Promise => { 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" } } ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 699b5b6..21c3bc3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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} diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index edaf427..7f8d3ce 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -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 ( diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index 4b21389..139b897 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -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({ diff --git a/src/sections/analysis/panelBody/analysisTab/opening.tsx b/src/sections/analysis/panelBody/analysisTab/opening.tsx index 6abc4e5..bf840fc 100644 --- a/src/sections/analysis/panelBody/analysisTab/opening.tsx +++ b/src/sections/analysis/panelBody/analysisTab/opening.tsx @@ -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); } diff --git a/src/sections/analysis/panelHeader/loadGame.tsx b/src/sections/analysis/panelHeader/loadGame.tsx index d3a737b..fe74090 100644 --- a/src/sections/analysis/panelHeader/loadGame.tsx +++ b/src/sections/analysis/panelHeader/loadGame.tsx @@ -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); }, diff --git a/src/sections/analysis/panelToolbar/index.tsx b/src/sections/analysis/panelToolbar/index.tsx index f49b758..a94854c 100644 --- a/src/sections/analysis/panelToolbar/index.tsx +++ b/src/sections/analysis/panelToolbar/index.tsx @@ -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() { resetBoard({ fen: getStartingFen({ game: board }) })} + onClick={() => resetBoard()} disabled={boardHistory.length === 0} sx={{ paddingX: 1.2, paddingY: 0.5 }} > diff --git a/src/sections/analysis/panelToolbar/nextMoveButton.tsx b/src/sections/analysis/panelToolbar/nextMoveButton.tsx index c851726..bad16d0 100644 --- a/src/sections/analysis/panelToolbar/nextMoveButton.tsx +++ b/src/sections/analysis/panelToolbar/nextMoveButton.tsx @@ -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]); diff --git a/src/sections/layout/index.tsx b/src/sections/layout/index.tsx index 506e6e3..c9b700a 100644 --- a/src/sections/layout/index.tsx +++ b/src/sections/layout/index.tsx @@ -35,7 +35,7 @@ export default function Layout({ children }: PropsWithChildren) { darkMode={useDarkMode} switchDarkMode={() => setDarkMode((val) => !val)} /> -
{children}
+
{children}
); } diff --git a/src/sections/play/board.tsx b/src/sections/play/board.tsx index 9867069..742705d 100644 --- a/src/sections/play/board.tsx +++ b/src/sections/play/board.tsx @@ -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);