UNPKG

@react-chess-tools/react-chess-puzzle

Version:

A lightweight, customizable React component library for rendering and interacting with chess puzzles.

609 lines (479 loc) 20 kB
<div align="center"> <h1>@react-chess-tools/react-chess-puzzle</h1> <p>A lightweight, customizable React component library for rendering and interacting with chess puzzles</p> [![npm version](https://img.shields.io/npm/v/@react-chess-tools/react-chess-puzzle.svg)](https://www.npmjs.com/package/@react-chess-tools/react-chess-puzzle) [![npm downloads](https://img.shields.io/npm/dm/@react-chess-tools/react-chess-puzzle.svg)](https://www.npmjs.com/package/@react-chess-tools/react-chess-puzzle) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/) </div> ## Table of Contents - [Overview](#overview) - [Features](#features) - [Installation](#installation) - [Quick Start](#quick-start) - [Demo](#demo) - [Puzzle Solving Flow](#puzzle-solving-flow) - [API Reference](#api-reference) - [ChessPuzzle.Root](#chesspuzzleroot) - [ChessPuzzle.Board](#chesspuzzleboard) - [ChessPuzzle.Reset](#chesspuzzlereset) - [ChessPuzzle.Hint](#chesspuzzlehint) - [Hooks](#hooks) - [useChessPuzzleContext](#usechesspuzzlecontext) - [useChessGameContext](#usechessgamecontext) - [Integration with react-chess-game](#integration-with-react-chess-game) - [Examples](#examples) - [License](#license) ## Overview `@react-chess-tools/react-chess-puzzle` is a React component library for creating interactive chess puzzle experiences. Built on top of [@react-chess-tools/react-chess-game](https://www.npmjs.com/package/@react-chess-tools/react-chess-game), it provides puzzle-specific features like move validation, hints, and progress tracking. ## Features - **Move Validation** - Automatically validates moves against the puzzle solution - **Hints** - Show the next correct move to help users - **Progress Tracking** - Track puzzle state (not-started, in-progress, solved, failed) - **Callbacks** - React to puzzle solve/fail events - **Built-in Reset** - Easily restart puzzles or load new ones - **Sound Effects** - Integrates with ChessGame.Sounds for audio feedback - **Keyboard Controls** - Navigate through puzzle moves with keyboard - **TypeScript** - Full TypeScript support with comprehensive type definitions - **Multiple solutions** - Accept any checkmate as a solution (configurable via `solveOnCheckmate`) ## Styling All components accept standard HTML attributes (`className`, `style`, `id`, `data-*`, `aria-*`), making them compatible with any CSS approach: ### Tailwind CSS ```tsx const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], makeFirstMove: false, }; <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board className="rounded-lg shadow-lg" /> <div className="flex gap-2 mt-4"> <ChessPuzzle.Reset className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> Try Again </ChessPuzzle.Reset> <ChessPuzzle.Hint className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"> Show Hint </ChessPuzzle.Hint> </div> </ChessPuzzle.Root>; ``` ### CSS Modules ```tsx import styles from "./Puzzle.module.css"; <ChessPuzzle.Board className={styles.board} />; ``` ### Custom Theme See the [ChessPuzzle.Root props](#chesspuzzleroot) for the `theme` prop to customize puzzle-specific colors (success, failure, hint). ## Installation ```bash npm install @react-chess-tools/react-chess-puzzle ``` ```bash yarn add @react-chess-tools/react-chess-puzzle ``` ```bash pnpm add @react-chess-tools/react-chess-puzzle ``` ## Quick Start ```tsx import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle"; function App() { const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], makeFirstMove: false, }; return ( <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Reset>Restart</ChessPuzzle.Reset> <ChessPuzzle.Hint>Get Hint</ChessPuzzle.Hint> </ChessPuzzle.Root> ); } ``` ## Demo Visit the [live demo](https://react-chess-tools.vercel.app/) to see the component in action. ## Puzzle Solving Flow 1. **Initial Setup** - The board displays the position from the FEN string 2. **First Move** - If `makeFirstMove` is `true`, the component automatically plays the first move 3. **User Interaction** - The user attempts to solve the puzzle by making moves 4. **Validation** - Each move is validated against the solution: - Correct move: The puzzle continues, opponent's response is auto-played - Incorrect move: The puzzle is marked as failed 5. **Completion** - When all correct moves are made, the puzzle is marked as solved ## API Reference ### ChessPuzzle.Root The root component that provides puzzle context to all child components. **Note:** This is a logic-only component (Context Provider). It does not render any DOM elements. #### Props | Name | Type | Default | Description | | ------------------ | --------------------------------------- | ------- | ----------------------------------------------- | | `puzzle` | `Puzzle` | - | The puzzle configuration (required) | | `onSolve` | `(ctx: ChessPuzzleContextType) => void` | - | Callback when puzzle is solved | | `onFail` | `(ctx: ChessPuzzleContextType) => void` | - | Callback when an incorrect move is made | | `solveOnCheckmate` | `boolean` | `true` | When true, any checkmate move solves the puzzle | | `theme` | `PartialChessPuzzleTheme` | - | Optional theme configuration | | `children` | `ReactNode` | - | Child components | #### Puzzle Object | Property | Type | Default | Description | | --------------- | ---------- | ------- | ------------------------------------------- | | `fen` | `string` | - | Initial position in FEN notation | | `moves` | `string[]` | - | Solution moves in algebraic or UCI notation | | `makeFirstMove` | `boolean` | `false` | Whether to auto-play the first move | #### Example ```tsx <ChessPuzzle.Root puzzle={{ fen: "4kb1r/p2r1ppp/4qn2/1B2p1B1/4P3/1Q6/PPP2PPP/2KR4 w k - 0 1", moves: ["Bxd7+", "Nxd7", "Qb8+", "Nxb8", "Rd8#"], makeFirstMove: false, }} onSolve={(ctx) => console.log("Solved!", ctx.movesPlayed)} onFail={(ctx) => console.log("Failed at move", ctx.movesPlayed)} > <ChessPuzzle.Board /> </ChessPuzzle.Root> ``` ### ChessPuzzle.Board Renders the chess board. Delegates to `ChessGame.Board` under the hood. Supports **ref forwarding** and all standard **HTML div attributes** (className, style, id, data-_, aria-_, etc.). #### Props | Name | Type | Description | | ----------- | -------------------------------- | --------------------------------------------- | | `options` | `ChessboardOptions` | Options forwarded to `react-chessboard` v5 | | `ref` | `Ref<HTMLDivElement>` | Forwarded ref to the underlying board element | | `className` | `string` | Custom CSS class names | | `style` | `CSSProperties` | Custom inline styles | | `...` | `HTMLAttributes<HTMLDivElement>` | All standard HTML div attributes | #### Example ```tsx <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board options={{ showNotation: true, animationDurationInMs: 200, }} className="puzzle-board" style={{ boxShadow: "0 4px 6px rgba(0,0,0,0.1)" }} /> </ChessPuzzle.Root> ``` ### ChessPuzzle.Reset A button component that resets the current puzzle or loads a new one. Supports **ref forwarding**, **asChild pattern**, and all standard **HTML button attributes** (className, style, disabled, etc.). #### Props | Name | Type | Default | Description | | ----------- | ----------------------------------------- | ----------------------------------- | --------------------------------------------------- | | `puzzle` | `Puzzle` | - | New puzzle to load (resets current if not provided) | | `onReset` | `(ctx: ChessPuzzleContextType) => void` | - | Callback after reset | | `showOn` | `Status[]` | `["failed", "solved"]` | States in which the button is visible | | `asChild` | `boolean` | `false` | Render as child element (slot pattern) | | `ref` | `Ref<HTMLButtonElement>` | Forwarded ref to the button element | | `className` | `string` | Custom CSS class names | | `...` | `ButtonHTMLAttributes<HTMLButtonElement>` | All standard HTML button attributes | **Status values:** `"not-started"`, `"in-progress"`, `"solved"`, `"failed"` #### Example ```tsx <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Reset>Try Again</ChessPuzzle.Reset> <ChessPuzzle.Reset puzzle={nextPuzzle}>Next Puzzle</ChessPuzzle.Reset> </ChessPuzzle.Root> ``` #### Using asChild Pattern ```tsx // Render as a custom button component import { MyCustomButton } from './MyButton'; <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Reset asChild> <MyCustomButton variant="primary">Try Again</MyCustomButton> </ChessPuzzle.Reset> </ChessPuzzle.Root> // Render as a link <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Reset asChild> <a href="#" onClick={(e) => e.preventDefault()}> Restart Puzzle </a> </ChessPuzzle.Reset> </ChessPuzzle.Root> ``` #### Event Handler Composition When using `asChild`, both your component's onClick and the Reset's onClick handler will work together: ```tsx <ChessPuzzle.Reset asChild> <button onClick={() => console.log("Custom handler")} className="custom-btn"> Try Again </button> </ChessPuzzle.Reset> // Both custom handler and reset logic will execute ``` ### ChessPuzzle.Hint A button that highlights the next correct move on the board. Supports **ref forwarding**, **asChild pattern**, and all standard **HTML button attributes** (className, style, disabled, etc.). #### Props | Name | Type | Default | Description | | ----------- | ----------------------------------------- | ----------------------------------- | -------------------------------------- | | `showOn` | `Status[]` | `["not-started", "in-progress"]` | States in which the button is visible | | `asChild` | `boolean` | `false` | Render as child element (slot pattern) | | `ref` | `Ref<HTMLButtonElement>` | Forwarded ref to the button element | | `className` | `string` | Custom CSS class names | | `...` | `ButtonHTMLAttributes<HTMLButtonElement>` | All standard HTML button attributes | #### Example ```tsx const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], makeFirstMove: false, }; <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Hint>Show Hint</ChessPuzzle.Hint> </ChessPuzzle.Root>; ``` #### Using asChild Pattern ```tsx <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Hint asChild> <button className="hint-btn">💡 Show Hint</button> </ChessPuzzle.Hint> </ChessPuzzle.Root> ``` ## Hooks ### useChessPuzzleContext Access the puzzle state and methods from any child component. ```tsx import { useChessPuzzleContext } from "@react-chess-tools/react-chess-puzzle"; function PuzzleStatus() { const { puzzleState, movesPlayed, totalMoves, resetPuzzle, onHint } = useChessPuzzleContext(); return ( <div> <p>Status: {puzzleState}</p> <p> Progress: {movesPlayed}/{totalMoves} moves </p> <button onClick={resetPuzzle}>Reset</button> <button onClick={onHint}>Hint</button> </div> ); } ``` #### Return Values | Property | Type | Description | | -------------- | -------------------------- | -------------------------------------- | | `status` | `Status` | Current puzzle state | | `puzzleState` | `Status` | Alias for `status` | | `movesPlayed` | `number` | Number of correct moves made | | `totalMoves` | `number` | Total moves in the solution | | `puzzle` | `Puzzle` | The current puzzle object | | `hint` | `Hint` | Current hint state | | `nextMove` | `string \| null` | The next correct move | | `isPlayerTurn` | `boolean` | Whether it's the player's turn to move | | `changePuzzle` | `(puzzle: Puzzle) => void` | Load a new puzzle | | `resetPuzzle` | `() => void` | Reset the current puzzle | | `onHint` | `() => void` | Show hint for next move | ### useChessGameContext Since `react-chess-puzzle` is built on `react-chess-game`, you can also access the underlying game context. ```tsx import { useChessGameContext } from "@react-chess-tools/react-chess-game"; function BoardInfo() { const { currentFen, info, methods } = useChessGameContext(); return ( <div> <p>Turn: {info.turn === "w" ? "White" : "Black"}</p> <button onClick={() => methods.flipBoard()}>Flip Board</button> </div> ); } ``` ## Integration with react-chess-game Since `react-chess-puzzle` is built on `react-chess-game`, you can use any of its components: ### Adding Sound Effects ```tsx import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle"; import { ChessGame } from "@react-chess-tools/react-chess-game"; function PuzzleWithSounds() { const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], }; return ( <ChessPuzzle.Root puzzle={puzzle}> <ChessGame.Sounds /> <ChessPuzzle.Board /> </ChessPuzzle.Root> ); } ``` ### Adding Keyboard Controls ```tsx import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle"; import { ChessGame } from "@react-chess-tools/react-chess-game"; function PuzzleWithKeyboard() { const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], }; return ( <ChessPuzzle.Root puzzle={puzzle}> <ChessGame.KeyboardControls /> <ChessPuzzle.Board /> </ChessPuzzle.Root> ); } ``` ## Examples ### Basic Puzzle ```tsx import { ChessPuzzle } from "@react-chess-tools/react-chess-puzzle"; function BasicPuzzle() { const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], makeFirstMove: false, }; return ( <ChessPuzzle.Root puzzle={puzzle}> <ChessPuzzle.Board /> <ChessPuzzle.Reset>Restart</ChessPuzzle.Reset> <ChessPuzzle.Hint>Hint</ChessPuzzle.Hint> </ChessPuzzle.Root> ); } ``` ### Puzzle with Callbacks ```tsx import { ChessPuzzle, type ChessPuzzleContextType, } from "@react-chess-tools/react-chess-puzzle"; import { useState } from "react"; function PuzzleWithScore() { const [score, setScore] = useState(0); const handleSolve = (ctx: ChessPuzzleContextType) => { setScore((prev) => prev + 10); console.log(`Solved in ${ctx.movesPlayed} moves!`); }; const handleFail = (ctx: ChessPuzzleContextType) => { setScore((prev) => Math.max(0, prev - 5)); console.log("Incorrect move!"); }; return ( <div> <p>Score: {score}</p> <ChessPuzzle.Root puzzle={puzzle} onSolve={handleSolve} onFail={handleFail} > <ChessPuzzle.Board /> <ChessPuzzle.Reset>Try Again</ChessPuzzle.Reset> </ChessPuzzle.Root> </div> ); } ``` ### Puzzle Trainer with Multiple Puzzles ```tsx import { ChessPuzzle, type ChessPuzzleContextType, } from "@react-chess-tools/react-chess-puzzle"; import { ChessGame } from "@react-chess-tools/react-chess-game"; import { useState } from "react"; const puzzles = [ { fen: "4kb1r/p2r1ppp/4qn2/1B2p1B1/4P3/1Q6/PPP2PPP/2KR4 w k - 0 1", moves: ["Bxd7+", "Nxd7", "Qb8+", "Nxb8", "Rd8#"], makeFirstMove: false, }, { fen: "6k1/5p1p/p1q1p1p1/1pB1P3/1Pr3Pn/P4P1P/4Q3/3R2K1 b - - 0 31", moves: ["h4f3", "e2f3", "c4c5", "d1d8", "g8g7", "f3f6"], makeFirstMove: true, }, ]; function PuzzleTrainer() { const [currentIndex, setCurrentIndex] = useState(0); const [score, setScore] = useState(0); const nextPuzzle = () => { setCurrentIndex((prev) => (prev + 1) % puzzles.length); }; const handleSolve = (ctx: ChessPuzzleContextType) => { setScore((prev) => prev + 10); nextPuzzle(); }; const handleFail = () => { setScore((prev) => Math.max(0, prev - 5)); nextPuzzle(); }; return ( <div className="puzzle-trainer"> <div className="score">Score: {score}</div> <ChessPuzzle.Root puzzle={puzzles[currentIndex]} onSolve={handleSolve} onFail={handleFail} > <ChessGame.Sounds /> <ChessGame.KeyboardControls /> <ChessPuzzle.Board /> <div className="controls"> <ChessPuzzle.Reset>Restart</ChessPuzzle.Reset> <ChessPuzzle.Hint>Hint</ChessPuzzle.Hint> <ChessPuzzle.Reset puzzle={puzzles[(currentIndex + 1) % puzzles.length]} > Skip </ChessPuzzle.Reset> </div> </ChessPuzzle.Root> </div> ); } ``` ### Custom Status Display ```tsx import { ChessPuzzle, useChessPuzzleContext, } from "@react-chess-tools/react-chess-puzzle"; function PuzzleStatusDisplay() { const { puzzleState, movesPlayed, totalMoves } = useChessPuzzleContext(); const messages = { "not-started": "Make your move to start", "in-progress": `Progress: ${movesPlayed}/${totalMoves} moves`, solved: "Puzzle solved! Well done!", failed: "Incorrect move. Try again!", }; return <div className={`status ${puzzleState}`}>{messages[puzzleState]}</div>; } function ResetLabel() { const { puzzleState } = useChessPuzzleContext(); return puzzleState === "solved" ? "Next Puzzle" : "Try Again"; } function PuzzleWithStatus() { const puzzle = { fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", moves: ["d2d4", "e5d4", "f3d4"], }; return ( <ChessPuzzle.Root puzzle={puzzle}> <PuzzleStatusDisplay /> <ChessPuzzle.Board /> <ChessPuzzle.Reset showOn={["solved", "failed"]}> <ResetLabel /> </ChessPuzzle.Reset> </ChessPuzzle.Root> ); } ``` ## License This project is [MIT](https://opensource.org/licenses/MIT) licensed. ## Show Your Support Give a star if this project helped you!