Create a puzzle with react-dnd

Emily Chen
Emily Chen
5 min readJun 9, 2024

--

When it comes to implementing a dragging function, the react-dnd or react-draggable library are common tools to fulfill this need. Today, let’s discuss how to use react-dnd to create a basic puzzle game in React.

First, we sketch the basic layout of the puzzle game. The first 3x3 board(Board list) is a shuffled puzzle which users can drag on, while the second one(Puzzle list) is an empty board where the puzzle pieces can be placed, which is also a 3x3 board with corresponding indexes.

These indexes are instrumental in ensuring that any piece dropped by the mouse on the board can be accurately placed. Lastly, we have a clear button placed at the end of the page, which users can click on to clear the current placement on the puzzle board.

photo credit: pixabay

There are 3 main actions: shuffle board when entering page or reloading, remove and add a piece when dragging to puzzle list, and clear the board to return to the initial sorted state.

// save curr item's index into a temp list
const findIndexAdapter = (list) => {
return list.reduce((indices, item, index) => {
if (item.id !== undefined) {
indices.push(index);
}
return indices;
}, []);
};

const checkPuzzleComplete = (puzzle) => {
for (let i = 0; i < puzzle.length; i++) {
if (puzzle[i].id !== i + 1) {
return false;
}
}
return true;
};

const puzzleReducer = (state = initialState, action) => {
switch (action.type) {
case "SHUFFLE_BOARD":
const shuffledBoard = [...action.payload].sort(() => Math.random() - 0.5);

return {
...state,
initialBoard: shuffledBoard,
board: shuffledBoard,
};

case "ADD_IMAGE_TO_BOARD":
const pictureIdToAdd = action.payload;
const targetPosition = action.position;

const pictureToRemoveIndex = state.board.findIndex(
(item) => item.id === pictureIdToAdd
);

if (pictureToRemoveIndex !== -1) {
const isDuplicate = state.puzzleItemList.includes(targetPosition);

if (!isDuplicate) {
const newBoard = state.board.map((item, index) => {
return index === pictureToRemoveIndex
? { ...state.board[pictureToRemoveIndex], url: "" }
: item;
});

const newPuzzle = state.puzzle.map((item, index) => {
return index === targetPosition
? state.board[pictureToRemoveIndex]
: item;
});

const isComplete =
state.puzzleItemList.length === 8
? checkPuzzleComplete(newPuzzle)
: null;

return {
...state,
board: newBoard, // delete drag item
puzzle: newPuzzle, // add drag item
puzzleItemList: findIndexAdapter(newPuzzle),
isPuzzleComplete: isComplete,
};
}
}
return state;

case "CLEAR_BOARD":
return {
...state,
initialBoard: [...state.initialBoard],
board: [...state.initialBoard],
puzzle: Array.from({ length: 9 }, () => ({})),
puzzleItemList: [],
isPuzzleComplete: null,
};
default:
return state;
}
};

export default puzzleReducer;

Picture Component

Each item can be created by a Picture component, using useDrag to set picture as a drag source.

import React from "react";
import { useDrag } from "react-dnd";

const Picture = ({ id, url }) => {
const [{ isDragging }, drag] = useDrag(() => ({
type: "image",
item: { id: id },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));

return (
<img
ref={drag}
src={url}
style={{ border: isDragging ? "2px solid #8DECB4" : "0px" }}
alt=""
/>
);
};

export default Picture;

Dropping Items on the Puzzle Board

We add a useDrop ref as a way to pass in the addImageToBoard action, which provides some object members that allow us to perform certain actions. For example, getClientOffset() method from the DropTargetMonitor returns the last recorded { x, y } client offset of the pointer.

However, you may wonder how to calculate the position of the puzzle where the mouse drop. Let’s dive into the process:

1. Calculate the center point of the screen.

2. Calculate the distance between mouse drop position and the center point.

3. Find the X, Y position of the puzzle where the mouse drop belongs.

4. Get the puzzle index from X, Y position.

  1. Since the puzzle game is justified to the center of the screen, the way to find the mouse drop position is to relocate the base point to the center point. Imagine (0,0) is the coordinate at the top left corner; we now adjust the coordinate to the center. window.innerWidth and window.innerHeight return the interior width and height of the window in pixels. Divide both by half allows us to find the center { x, y } coordinates.
  2. As getClientOffSet() gives us the client offset of the pointer, we can calculate the distance between the offset { x, y } and the center { x, y }. Let’s named it adjusted { x, y }.
  3. Since we divide the screen to a half to get the center point, the { x, y } position of the mouse in the 3x3 board should also add 150 pixels(300/2), which helps shift the origin of the coordinate system from the center to the top-left corner. Then, we divide the adjusted coordinates by 100 and floor the result to get the integer cell index.
const [, drop] = useDrop(() => ({
accept: "image",
drop: (item, monitor) => {
const offset = monitor.getClientOffset();
if (offset) {
// calculate the center X, Y
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const centerX = screenWidth / 2;
const centerY = screenHeight / 2;

// calculate the diff between mouse point to center
const adjustedX = offset.x - centerX;
const adjustedY = offset.y - centerY;

// calculate x, y position
const x = Math.floor((adjustedX + 150) / 100);
const y = Math.floor((adjustedY + 150) / 100);

// get the puzzle index from X, Y position
const position = y * 3 + x - 2;

dispatch(addImageToBoard(item.id, position - 1));
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));

Finally, once we get the mouse drop index from the puzzle board, ADD_IMAGE_TO_BOARD action can be dispatched, passing in the item index of the dragged item and the index of mouse drop. We then successfully drop a non-duplicated item on the PuzzleList board.

For future optimization, it would be better to add a feature where the user can upload an image greater than 300x300, then provide a crop and save function to crop into a 300x300 size(which can be adjust to fit a customized puzzle board size), slice the image to 9 pieces, save the pieces to a folder, and use those pieces to fulfill a more thorough puzzle game.

Demo

unsuccessful puzzle drop

codeSandBox

Github

--

--