diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index cb931c3d..9bf34b66 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -1,4 +1,4 @@ -import { getGameFromPgn, setGameHeaders } from "@/lib/chess"; +import { getGameFromFen, getGameFromPgn, setGameHeaders } from "@/lib/chess"; import { playIllegalMoveSound, playSoundFromMove } from "@/lib/sounds"; import { Player } from "@/types/game"; import { Chess, Move, DEFAULT_POSITION } from "chess.js"; @@ -11,6 +11,7 @@ export interface resetGameParams { black?: Player; noHeaders?: boolean; } +type GameSource = "fen" | "pgn"; export const useChessActions = (chessAtom: PrimitiveAtom) => { const [game, setGame] = useAtom(chessAtom); @@ -53,9 +54,31 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { }, [game]); const resetToStartingPosition = useCallback( - (pgn?: string) => { - const newGame = pgn ? getGameFromPgn(pgn) : copyGame(); - newGame.load(newGame.getHeaders().FEN || DEFAULT_POSITION, { + (input?: string, source?: GameSource) => { + let newGame: Chess; + + if (input) { + if (source === "fen") { + newGame = getGameFromFen(input); + } else if (source === "pgn") { + newGame = getGameFromPgn(input); + } else { + newGame = getGameFromPgn(input); + } + } else { + newGame = copyGame(); + } + + const currentHeaders = newGame.getHeaders(); + let fenToLoad = DEFAULT_POSITION; + + if (source === "fen" && input) { + fenToLoad = input; + } else if (currentHeaders.FEN) { + fenToLoad = currentHeaders.FEN; + } + + newGame.load(fenToLoad, { preserveHeaders: true, }); setGame(newGame); diff --git a/src/lib/chess.ts b/src/lib/chess.ts index af3f0f5f..ae6eb6f7 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -25,6 +25,27 @@ export const getGameFromPgn = (pgn: string): Chess => { return game; }; +export const getGameFromFen = (fenInput: string): Chess => { + const game = new Chess(); + + let fen = fenInput.trim(); + + const parts = fen.split(/\s+/); + + if (parts.length < 6) { + const defaults = ["w", "-", "-", "0", "1"]; + const missingDefaults = defaults.slice(parts.length - 1); + fen = `${fen} ${missingDefaults.join(" ")}`; + } + + game.load(fen); + + game.setHeader("FEN", fen); + game.setHeader("SetUp", "1"); + + return game; +}; + export const formatGameToDatabase = (game: Chess): Omit => { const headers: Record = game.getHeaders(); diff --git a/src/sections/analysis/panelHeader/loadGame.tsx b/src/sections/analysis/panelHeader/loadGame.tsx index 30cd657c..c1b6e697 100644 --- a/src/sections/analysis/panelHeader/loadGame.tsx +++ b/src/sections/analysis/panelHeader/loadGame.tsx @@ -18,7 +18,7 @@ import { fetchLichessGame } from "@/lib/lichess"; export default function LoadGame() { const router = useRouter(); const game = useAtomValue(gameAtom); - const { setPgn: setGamePgn } = useChessActions(gameAtom); + const { setPgn: setGamePgn, reset: resetGame } = useChessActions(gameAtom); const { resetToStartingPosition: resetBoard } = useChessActions(boardAtom); const { gameFromUrl } = useGameDatabase(); const setEval = useSetAtom(gameEvalAtom); @@ -28,17 +28,51 @@ export default function LoadGame() { const joinedGameHistory = useMemo(() => game.history().join(), [game]); const resetAndSetGamePgn = useCallback( - (pgn: string, orientation?: boolean, gameEval?: GameEval) => { - const gameFromPgn = new Chess(); - gameFromPgn.loadPgn(pgn); - if (joinedGameHistory === gameFromPgn.history().join()) return; + ( + input: string, + orientation?: boolean, + gameEval?: GameEval, + source: "fen" | "pgn" = "pgn" + ) => { + const gameFromInput = new Chess(); - resetBoard(pgn); + try { + if (source === "fen") { + gameFromInput.load(input); + } else { + gameFromInput.loadPgn(input); + } + } catch (e) { + console.error("Error while loading the game:", e); + return; + } + + if ( + source === "pgn" && + joinedGameHistory === gameFromInput.history().join() + ) { + return; + } + + resetBoard(input, source); setEval(gameEval); - setGamePgn(pgn); + + if (source === "fen") { + resetGame({ fen: input }); + } else { + setGamePgn(input); + } + setBoardOrientation(orientation ?? true); }, - [joinedGameHistory, resetBoard, setGamePgn, setEval, setBoardOrientation] + [ + joinedGameHistory, + resetBoard, + setGamePgn, + resetGame, + setEval, + setBoardOrientation, + ] ); const { lichessGameId, orientation: orientationParam } = router.query; @@ -101,7 +135,17 @@ export default function LoadGame() { undefined, { shallow: true, scroll: false } ); - resetAndSetGamePgn(game.pgn()); + const headers = game.getHeaders(); + const isFenConfiguration = headers["SetUp"] === "1"; + const hasNoMoves = game.history().length === 0; + + if (isFenConfiguration || hasNoMoves) { + const fen = game.fen(); + resetAndSetGamePgn(fen, undefined, undefined, "fen"); + } else { + const pgn = game.pgn(); + resetAndSetGamePgn(pgn, undefined, undefined, "pgn"); + } }} /> ); diff --git a/src/sections/loadGame/gameFenInput.tsx b/src/sections/loadGame/gameFenInput.tsx new file mode 100644 index 00000000..3db7c57c --- /dev/null +++ b/src/sections/loadGame/gameFenInput.tsx @@ -0,0 +1,44 @@ +import { TextField, InputAdornment, IconButton, Tooltip } from "@mui/material"; +import { Icon } from "@iconify/react"; +import React from "react"; + +interface Props { + fen: string; + setFen: (fen: string) => void; +} + +export default function GameFenInput({ fen, setFen }: Props) { + const handleClipboardPaste = async () => { + try { + const text = await navigator.clipboard.readText(); + if (text) setFen(text); + } catch (err) { + console.error("Failed to read clipboard", err); + } + }; + + return ( + setFen(e.target.value)} + placeholder="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + // flex: 1 faz com que ele preencha o espaço restante ao lado do Select + sx={{ flex: 1 }} + slotProps={{ + input: { + endAdornment: ( + + + + + + + + ), + }, + }} + /> + ); +} diff --git a/src/sections/loadGame/loadGameDialog.tsx b/src/sections/loadGame/loadGameDialog.tsx index fcbe386e..e8e28eef 100644 --- a/src/sections/loadGame/loadGameDialog.tsx +++ b/src/sections/loadGame/loadGameDialog.tsx @@ -1,5 +1,5 @@ import { useGameDatabase } from "@/hooks/useGameDatabase"; -import { getGameFromPgn } from "@/lib/chess"; +import { getGameFromFen, getGameFromPgn } from "@/lib/chess"; import { GameOrigin } from "@/types/enums"; import { MenuItem, @@ -25,6 +25,7 @@ import { useLocalStorage } from "@/hooks/useLocalStorage"; import LichessInput from "./lichessInput"; import { useSetAtom } from "jotai"; import { boardOrientationAtom } from "../analysis/states"; +import GameFenInput from "./gameFenInput"; interface Props { open: boolean; @@ -34,6 +35,7 @@ interface Props { export default function NewGameDialog({ open, onClose, setGame }: Props) { const [pgn, setPgn] = useState(""); + const [fen, setFen] = useState(""); const [gameOrigin, setGameOrigin] = useLocalStorage( "preferred-game-origin", GameOrigin.ChessCom @@ -43,12 +45,19 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { const setBoardOrientation = useSetAtom(boardOrientationAtom); const { addGame } = useGameDatabase(); - const handleAddGame = async (pgn: string, boardOrientation?: boolean) => { - if (!pgn) return; + const handleAddGame = async (input: string, boardOrientation?: boolean) => { + if (!input) return; try { - const gameToAdd = getGameFromPgn(pgn); - setSentryContext("loadedGame", { pgn }); + let gameToAdd: Chess; + + if (gameOrigin === GameOrigin.Fen) { + gameToAdd = getGameFromFen(input); + setSentryContext("loadedGame", { fen: input }); + } else { + gameToAdd = getGameFromPgn(input); + setSentryContext("loadedGame", { pgn: input }); + } if (setGame) { await setGame(gameToAdd); @@ -139,6 +148,10 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { )} + {gameOrigin === GameOrigin.Fen && ( + + )} + {gameOrigin === GameOrigin.ChessCom && ( )} @@ -163,12 +176,12 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) { - {gameOrigin === GameOrigin.Pgn && ( + {(gameOrigin === GameOrigin.Pgn || gameOrigin === GameOrigin.Fen) && (