@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
Markdown
<div align="center">
<h1>-chess-tools/react-chess-puzzle</h1>
<p>A lightweight, customizable React component library for rendering and interacting with chess puzzles</p>
[](https://www.npmjs.com/package/@react-chess-tools/react-chess-puzzle)
[](https://www.npmjs.com/package/@react-chess-tools/react-chess-puzzle)
[](https://opensource.org/licenses/MIT)
[](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
`-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 -chess-tools/react-chess-puzzle
```
```bash
yarn add -chess-tools/react-chess-puzzle
```
```bash
pnpm add -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!