@react-chess-tools/react-chess-puzzle
Version:
A lightweight, customizable React component library for rendering and interacting with chess puzzles.
497 lines (482 loc) • 13.8 kB
JavaScript
// src/components/ChessPuzzle/parts/Root.tsx
import React3 from "react";
// src/utils/index.ts
import { Chess } from "chess.js";
import _ from "lodash";
// src/theme/defaults.ts
import { defaultGameTheme } from "@react-chess-tools/react-chess-game";
var defaultPuzzleTheme = {
...defaultGameTheme,
puzzle: {
success: "rgba(172, 206, 89, 0.5)",
failure: "rgba(201, 52, 48, 0.5)",
hint: "rgba(27, 172, 166, 0.5)"
}
};
// src/utils/index.ts
var getOrientation = (puzzle) => {
const fen = puzzle.fen;
const game = new Chess(fen);
if (puzzle.makeFirstMove) {
game.move(puzzle.moves[0]);
}
return game.turn();
};
var getCustomSquareStyles = (status, hint, isPlayerTurn, game, nextMove, theme = defaultPuzzleTheme) => {
const customSquareStyles = {};
const lastMove = _.last(game.history({ verbose: true }));
if (status === "failed" && lastMove) {
customSquareStyles[lastMove.from] = {
backgroundColor: theme.puzzle.failure
};
customSquareStyles[lastMove.to] = {
backgroundColor: theme.puzzle.failure
};
}
if (lastMove && (status === "solved" || status !== "failed" && !isPlayerTurn)) {
customSquareStyles[lastMove.from] = {
backgroundColor: theme.puzzle.success
};
customSquareStyles[lastMove.to] = {
backgroundColor: theme.puzzle.success
};
}
if (hint === "piece") {
if (nextMove) {
customSquareStyles[nextMove.from] = {
backgroundColor: theme.puzzle.hint
};
}
}
if (hint === "move") {
if (nextMove) {
customSquareStyles[nextMove.from] = {
backgroundColor: theme.puzzle.hint
};
customSquareStyles[nextMove.to] = {
backgroundColor: theme.puzzle.hint
};
}
}
return customSquareStyles;
};
var stringToMove = (game, move) => {
const copy = new Chess(game.fen());
if (move === null || move === void 0) {
return null;
}
try {
return copy.move(move);
} catch (e) {
return null;
}
};
// src/hooks/useChessPuzzle.ts
import { useEffect, useReducer, useCallback, useMemo } from "react";
// src/hooks/reducer.ts
var initializePuzzle = ({ puzzle }) => {
return {
puzzle,
currentMoveIndex: 0,
status: "not-started",
nextMove: puzzle.moves[0],
hint: "none",
cpuMove: null,
needCpuMove: !!puzzle.makeFirstMove,
isPlayerTurn: !puzzle.makeFirstMove,
onSolveInvoked: false,
onFailInvoked: false
};
};
var reducer = (state, action) => {
switch (action.type) {
case "INITIALIZE":
return {
...state,
...initializePuzzle(action.payload)
};
case "RESET":
return {
...state,
...initializePuzzle({
puzzle: state.puzzle
})
};
case "TOGGLE_HINT":
if (state.hint === "none") {
return { ...state, hint: "piece" };
}
return { ...state, hint: "move" };
case "CPU_MOVE":
if (state.isPlayerTurn) {
return state;
}
if (["solved", "failed"].includes(state.status)) {
return state;
}
return {
...state,
currentMoveIndex: state.currentMoveIndex + 1,
cpuMove: state.puzzle.moves[state.currentMoveIndex],
nextMove: state.currentMoveIndex < state.puzzle.moves.length - 1 ? state.puzzle.moves[state.currentMoveIndex + 1] : null,
needCpuMove: false,
isPlayerTurn: true,
status: "in-progress"
};
case "PLAYER_MOVE": {
const { move, game, solveOnCheckmate } = action.payload;
if (move && solveOnCheckmate !== false && game.isCheckmate()) {
return {
...state,
status: "solved",
nextMove: null,
hint: "none",
isPlayerTurn: false,
onSolveInvoked: false
};
}
const isMoveRight = [move == null ? void 0 : move.san, move == null ? void 0 : move.lan].includes(
(state == null ? void 0 : state.nextMove) || ""
);
const isPuzzleSolved = state.currentMoveIndex === state.puzzle.moves.length - 1;
if (!isMoveRight) {
return {
...state,
status: "failed",
nextMove: null,
hint: "none",
isPlayerTurn: false,
onFailInvoked: false
};
}
if (isPuzzleSolved) {
return {
...state,
status: "solved",
nextMove: null,
hint: "none",
isPlayerTurn: false,
onSolveInvoked: false
};
}
return {
...state,
hint: "none",
currentMoveIndex: state.currentMoveIndex + 1,
nextMove: state.puzzle.moves[state.currentMoveIndex + 1],
status: "in-progress",
needCpuMove: true,
isPlayerTurn: false
};
}
case "MARK_SOLVE_INVOKED":
return {
...state,
onSolveInvoked: true
};
case "MARK_FAIL_INVOKED":
return {
...state,
onFailInvoked: true
};
default:
return state;
}
};
// src/hooks/useChessPuzzle.ts
import { useChessGameContext } from "@react-chess-tools/react-chess-game";
var useChessPuzzle = (puzzle, onSolve, onFail, solveOnCheckmate = true) => {
var _a;
const gameContext = useChessGameContext();
const [state, dispatch] = useReducer(reducer, { puzzle }, initializePuzzle);
const {
game,
methods: { makeMove, setPosition }
} = gameContext;
const changePuzzle = useCallback(
(puzzle2) => {
setPosition(puzzle2.fen, getOrientation(puzzle2));
dispatch({ type: "INITIALIZE", payload: { puzzle: puzzle2 } });
},
[setPosition]
);
useEffect(() => {
changePuzzle(puzzle);
}, [JSON.stringify(puzzle), changePuzzle]);
useEffect(() => {
if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {
setTimeout(
() => dispatch({
type: "CPU_MOVE"
}),
0
);
}
}, [gameContext, state.needCpuMove]);
useEffect(() => {
if (state.cpuMove) {
makeMove(state.cpuMove);
}
}, [state.cpuMove]);
if (!gameContext) {
throw new Error("useChessPuzzle must be used within a ChessGameContext");
}
const onHint = useCallback(() => {
dispatch({ type: "TOGGLE_HINT" });
}, []);
const resetPuzzle = useCallback(() => {
changePuzzle(puzzle);
}, [changePuzzle, puzzle]);
const puzzleContext = useMemo(
() => ({
status: state.status,
changePuzzle,
resetPuzzle,
puzzle,
hint: state.hint,
onHint,
nextMove: state.nextMove,
isPlayerTurn: state.isPlayerTurn,
puzzleState: state.status,
movesPlayed: state.currentMoveIndex,
totalMoves: puzzle.moves.length
}),
[
state.status,
changePuzzle,
resetPuzzle,
puzzle,
state.hint,
onHint,
state.nextMove,
state.isPlayerTurn,
state.currentMoveIndex
]
);
useEffect(() => {
var _a2, _b, _c;
if (((_a2 = game == null ? void 0 : game.history()) == null ? void 0 : _a2.length) <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
return;
}
if (game.history().length % 2 === (puzzle.makeFirstMove ? 0 : 1)) {
dispatch({
type: "PLAYER_MOVE",
payload: {
move: ((_c = (_b = gameContext == null ? void 0 : gameContext.game) == null ? void 0 : _b.history({ verbose: true })) == null ? void 0 : _c.pop()) ?? null,
puzzleContext,
game,
solveOnCheckmate
}
});
dispatch({
type: "CPU_MOVE"
});
}
}, [(_a = game == null ? void 0 : game.history()) == null ? void 0 : _a.length]);
useEffect(() => {
if (state.status === "solved" && !state.onSolveInvoked && onSolve) {
onSolve(puzzleContext);
dispatch({ type: "MARK_SOLVE_INVOKED" });
}
}, [state.status, state.onSolveInvoked]);
useEffect(() => {
if (state.status === "failed" && !state.onFailInvoked && onFail) {
onFail(puzzleContext);
dispatch({ type: "MARK_FAIL_INVOKED" });
}
}, [state.status, state.onFailInvoked]);
return puzzleContext;
};
// src/components/ChessPuzzle/parts/Root.tsx
import { ChessGame } from "@react-chess-tools/react-chess-game";
// src/hooks/useChessPuzzleContext.ts
import React from "react";
var ChessPuzzleContext = React.createContext(null);
var useChessPuzzleContext = () => {
const context = React.useContext(ChessPuzzleContext);
if (!context) {
throw new Error(
`useChessPuzzleContext must be used within a ChessPuzzle component. Make sure your component is wrapped with <ChessPuzzle.Root> or ensure the ChessPuzzle component is properly rendered in the component tree.`
);
}
return context;
};
// src/theme/context.tsx
import React2, { createContext, useContext } from "react";
var ChessPuzzleThemeContext = createContext(defaultPuzzleTheme);
var useChessPuzzleTheme = () => {
return useContext(ChessPuzzleThemeContext);
};
var PuzzleThemeProvider = ({
theme,
children
}) => {
return /* @__PURE__ */ React2.createElement(ChessPuzzleThemeContext.Provider, { value: theme }, children);
};
// src/theme/utils.ts
import { merge } from "lodash";
var mergePuzzleTheme = (partialTheme) => {
if (!partialTheme) {
return { ...defaultPuzzleTheme };
}
return merge({}, defaultPuzzleTheme, partialTheme);
};
// src/components/ChessPuzzle/parts/Root.tsx
var PuzzleRootInner = ({
puzzle,
onSolve,
onFail,
solveOnCheckmate,
children
}) => {
const context = useChessPuzzle(puzzle, onSolve, onFail, solveOnCheckmate);
return /* @__PURE__ */ React3.createElement(ChessPuzzleContext.Provider, { value: context }, children);
};
var Root = ({
puzzle,
onSolve,
onFail,
theme,
solveOnCheckmate = true,
children
}) => {
const mergedTheme = React3.useMemo(() => mergePuzzleTheme(theme), [theme]);
return /* @__PURE__ */ React3.createElement(
ChessGame.Root,
{
fen: puzzle.fen,
orientation: getOrientation(puzzle),
theme: mergedTheme
},
/* @__PURE__ */ React3.createElement(PuzzleThemeProvider, { theme: mergedTheme }, /* @__PURE__ */ React3.createElement(
PuzzleRootInner,
{
puzzle,
onSolve,
onFail,
solveOnCheckmate
},
children
))
);
};
Root.displayName = "ChessPuzzle.Root";
// src/components/ChessPuzzle/parts/PuzzleBoard.tsx
import React4 from "react";
import {
ChessGame as ChessGame2,
deepMergeChessboardOptions,
useChessGameContext as useChessGameContext2
} from "@react-chess-tools/react-chess-game";
var PuzzleBoard = React4.forwardRef(
({ options, ...rest }, ref) => {
const puzzleContext = useChessPuzzleContext();
const gameContext = useChessGameContext2();
const theme = useChessPuzzleTheme();
if (!puzzleContext) {
throw new Error("PuzzleContext not found");
}
if (!gameContext) {
throw new Error("ChessGameContext not found");
}
const { game } = gameContext;
const { status, hint, isPlayerTurn, nextMove } = puzzleContext;
const mergedOptions = deepMergeChessboardOptions(options || {}, {
squareStyles: getCustomSquareStyles(
status,
hint,
isPlayerTurn,
game,
stringToMove(game, nextMove),
theme
)
});
return /* @__PURE__ */ React4.createElement(ChessGame2.Board, { ref, ...rest, options: mergedOptions });
}
);
PuzzleBoard.displayName = "ChessPuzzle.PuzzleBoard";
// src/components/ChessPuzzle/parts/Reset.tsx
import React5 from "react";
import { Slot } from "@radix-ui/react-slot";
var defaultShowOn = ["failed", "solved"];
var Reset = React5.forwardRef(
({
children,
asChild,
puzzle,
onReset,
showOn = defaultShowOn,
className,
...rest
}, ref) => {
const puzzleContext = useChessPuzzleContext();
if (!puzzleContext) {
throw new Error("PuzzleContext not found");
}
const { changePuzzle, puzzle: contextPuzzle, status } = puzzleContext;
const handleClick = React5.useCallback(() => {
changePuzzle(puzzle || contextPuzzle);
onReset == null ? void 0 : onReset(puzzleContext);
}, [changePuzzle, puzzle, contextPuzzle, puzzleContext, onReset]);
if (!showOn.includes(status)) {
return null;
}
return asChild ? /* @__PURE__ */ React5.createElement(Slot, { ref, onClick: handleClick, className, ...rest }, children) : /* @__PURE__ */ React5.createElement(
"button",
{
ref,
type: "button",
className,
onClick: handleClick,
...rest
},
children
);
}
);
Reset.displayName = "ChessPuzzle.Reset";
// src/components/ChessPuzzle/parts/Hint.tsx
import React6 from "react";
import { Slot as Slot2 } from "@radix-ui/react-slot";
var defaultShowOn2 = ["not-started", "in-progress"];
var Hint = React6.forwardRef(({ children, asChild, showOn = defaultShowOn2, className, ...rest }, ref) => {
const puzzleContext = useChessPuzzleContext();
if (!puzzleContext) {
throw new Error("PuzzleContext not found");
}
const { onHint, status } = puzzleContext;
const handleClick = React6.useCallback(() => {
onHint();
}, [onHint]);
if (!showOn.includes(status)) {
return null;
}
return asChild ? /* @__PURE__ */ React6.createElement(Slot2, { ref, onClick: handleClick, className, ...rest }, children) : /* @__PURE__ */ React6.createElement(
"button",
{
ref,
type: "button",
className,
onClick: handleClick,
...rest
},
children
);
});
Hint.displayName = "ChessPuzzle.Hint";
// src/components/ChessPuzzle/index.ts
var ChessPuzzle = {
Root,
Board: PuzzleBoard,
Reset,
Hint
};
export {
ChessPuzzle,
ChessPuzzleThemeContext,
defaultPuzzleTheme,
mergePuzzleTheme,
useChessPuzzleContext,
useChessPuzzleTheme
};
//# sourceMappingURL=index.js.map