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
31 changes: 27 additions & 4 deletions src/hooks/useChessActions.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,6 +11,7 @@ export interface resetGameParams {
black?: Player;
noHeaders?: boolean;
}
type GameSource = "fen" | "pgn";

export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
const [game, setGame] = useAtom(chessAtom);
Expand Down Expand Up @@ -53,9 +54,31 @@ export const useChessActions = (chessAtom: PrimitiveAtom<Chess>) => {
}, [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);
Expand Down
21 changes: 21 additions & 0 deletions src/lib/chess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Game, "id"> => {
const headers: Record<string, string | undefined> = game.getHeaders();

Expand Down
62 changes: 53 additions & 9 deletions src/sections/analysis/panelHeader/loadGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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");
}
}}
/>
);
Expand Down
44 changes: 44 additions & 0 deletions src/sections/loadGame/gameFenInput.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TextField
label="FEN Position"
variant="outlined"
value={fen}
onChange={(e) => 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: (
<InputAdornment position="end">
<Tooltip title="Paste from Clipboard">
<IconButton edge="end" onClick={handleClipboardPaste}>
<Icon icon="ri:clipboard-line" />
</IconButton>
</Tooltip>
</InputAdornment>
),
},
}}
/>
);
}
28 changes: 21 additions & 7 deletions src/sections/loadGame/loadGameDialog.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -139,6 +148,10 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
<GamePgnInput pgn={pgn} setPgn={setPgn} />
)}

{gameOrigin === GameOrigin.Fen && (
<GameFenInput fen={fen} setFen={setFen} />
)}

{gameOrigin === GameOrigin.ChessCom && (
<ChessComInput onSelect={handleAddGame} />
)}
Expand All @@ -163,12 +176,12 @@ export default function NewGameDialog({ open, onClose, setGame }: Props) {
<Button variant="outlined" onClick={handleClose}>
Cancel
</Button>
{gameOrigin === GameOrigin.Pgn && (
{(gameOrigin === GameOrigin.Pgn || gameOrigin === GameOrigin.Fen) && (
<Button
variant="contained"
sx={{ marginLeft: 2 }}
onClick={() => {
handleAddGame(pgn);
handleAddGame(gameOrigin === GameOrigin.Fen ? fen : pgn);
}}
>
Add
Expand All @@ -183,4 +196,5 @@ const gameOriginLabel: Record<GameOrigin, string> = {
[GameOrigin.ChessCom]: "Chess.com",
[GameOrigin.Lichess]: "Lichess.org",
[GameOrigin.Pgn]: "PGN",
[GameOrigin.Fen]: "FEN",
};
1 change: 1 addition & 0 deletions src/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum GameOrigin {
Pgn = "pgn",
ChessCom = "chesscom",
Lichess = "lichess",
Fen = "fen",
}

export enum EngineName {
Expand Down