DEV Community

Cover image for Using React-dnd to create a Tower of Hanoi game
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Using React-dnd to create a Tower of Hanoi game

Written by Diogo Souza✏️

Drag and drop features have existed for many years.

Since the advent of jQuery and DOM manipulation, it’s gotten a lot easier to make things draggable and create places that can be droppable for them.

Nowadays, companies like Gmail, Dropbox, and Microsoft seem to be keen on using this well-established feature by utilizing tons of different libraries and techniques to achieve a variety of effects.

They also utilizing drag and drop features to allow their users to upload files.

It goes even beyond that to the point where UI/UX professionals can measure when this effect is needed based on their user’s preferences and behaviors.

When it comes to React, three main libraries seem to have embraced this world:

  • react-beautiful-dnd: the favorite among React devs (maybe because is under the Atlassian caring), it’s mature and has a wide community
  • react-dnd: the one this article is focused on because of its powerful API for handling dragging and dropping
  • react-grid-layout: the best choice for who relies on a lot of grids and complex stack structures

In this article, we’ll run away from the common usage of this lib, which is for file uploading or features alike.

Instead, we’ll develop a game: the famous Tower of Hanoi.

This is how it’ll look when we’re done:

a gif showing a finished tower of hanoi game built with React-dnd.
The final game.

Tower of Hanoi basics

If you’re not familiar with the puzzle, the Tower of Hanoi is a classic game played and developed by many students from Computer Science when first starting to learn how to program, especially because it’s easy to code.

an image from Wikipedia exemplifying the Tower of Hanoi game.

The game consists of three or more disks or tiles stacked on top of each other in one initial tower.

They start stacked from the largest to the smallest disk. You can remove them and drag them to another tower.

These are the rules:

  • You can move just one disk at a time
  • You can only move a disk to an empty tower or on top of a tower that already contains other disks
  • You can only place disks over larger disks.

The goal is to move the whole pile of disks from one tower to another in the fewest moves possible.

LogRocket Free Trial Banner

A bit about react-dnd

The documentation of react-dnd is very simple and easy to follow.

Before we proceed to the coding, we need first to understand some key concepts.

Backends

They’re the API under the abstraction of using drag and drop.

We have a common interface with functions that can be rewritten in any type of device, depending on which has implemented the abstraction.

For this tutorial, we’ll take advantage of the HTML5 drag and drop API as the backend for our game app.

Monitors

Dragging and dropping things is inherently connected to maintaining a state.

In other words, every time you drag a component from one place to another, you’re actually moving data around. Data needs to be saved in a state.

The monitors are the wrappers of that state, allowing you to recover and manage your component’s data as a result of dragging and dropping over the component.

Connectors

As the name suggests, we need something to connect both worlds: the React components and the DOM nodes that are, actually, performing the physical drag-and-drop operations.

It tells which, in the end, is a valid drag element or a drop target.

Drag sources and Drop targets

You’ll see soon that those are also the respective React component names for the dragging and dropping decorators.

They represent the primary abstraction of the APIs we’ve talked about, injecting the values and performing the callback operations of drag and drop.

High-order components and decorators

All of that logic needs to be encapsulated into higher components — the ones that represent logical divisions to you and your React architecture.

The high-order components take what they need to concatenate all the react-dnd operations of dragging and dropping and return a new component recognizable by the lib.

In other words, it’s the component class we’ll create that annotates the DnD logic and returns a valid DnD component.

Project setup

In order to follow through with this tutorial, you’ll need to have Node, npm, and npx properly installed and working on your machine. Go ahead and do that if you haven’t already.

We’re also going to use Yarn as the package manager since it is simple and straightforward. Make sure you have the latest version.

We’re going to make use of create-react-app for scaffolding our application and facilitating the initial configurations.

In the directory of your choice, run the following command:

npx create-react-app logrocket-hanoi-tower
cd logrocket-hanoi-tower
yarn start
Enter fullscreen mode Exit fullscreen mode

This will start the default application and open it in your browser.

Next, we need to add the react-dnd dependencies to our project.

To do that, run the following command into the root folder:

yarn add styled-components react-dnd react-dnd-html5-backend
Enter fullscreen mode Exit fullscreen mode

Note that we’re adding two other dependencies:

  • styled-components to facilitate our ability to impose styling on our components, since we don’t need to add explicit CSS files
  • react-dnd-html5-backend to work along with react-dnd for web browsers (not supported in mobile devices yet)

Show me the code

Now we’re going to look at the code.

But first, let me show the project architecture:

The project architecture of the LogRocket Tower of Hanoi game.

Project directories

We basically have three main folders. The first is for the components and the constants we’ll need to store data such as the heights of the tiles and towers, etc.

The second folder will hold the images, and the third will contain the styles. We also still have a CSS file for the body and general styling.

Let’s start with the constants since we’ll need them in the rest of the code.

Create a new JavaScript file called Constants.js and add the following code:

const NUM_TILES = 3;
const TOWER_WIDTH = `${30 * NUM_TILES}px`;
const HEADER_HEIGHT = "8rem";
const FOOTER_HEIGHT = "2rem";
const HANOI_HEIGHT = `(100vh - ${HEADER_HEIGHT} - ${FOOTER_HEIGHT})`;
const TOWER_HEIGHT = `(${TOWER_WIDTH} * ${NUM_TILES}) * 1.3`;
const TILE_HEIGHT = `(${TOWER_HEIGHT} / 12)`;

const getWidth = () => {
    switch (NUM_TILES) {
        case 1:
            return 13;
        case 2:
            return 10.5;
        case 3:
            return 8;
        default:
            return 3;
    }
};

const TILE_WIDTH_BASE = getWidth();

export default {
    TOWER_WIDTH,
    HEADER_HEIGHT,
    FOOTER_HEIGHT,
    HANOI_HEIGHT,
    TOWER_HEIGHT,
    TILE_HEIGHT,
    TILE_WIDTH_BASE,
    NUM_TILES
};
Enter fullscreen mode Exit fullscreen mode

There’s a lot here, but don’t be fooled: it’s just constants to set up the default and/or auto-generated values of heights, widths, and the number of tiles we’ll have.

Since the browser page will be our game background and each monitor has different dimensions, we need to calculate in real-time where each component will be placed — especially in the case of re-dimensioning and responsive responses.

For the sake of simplicity, our game will only have a maximum 3 tiles.

However, you can change this constant at any time and see how the game behaves with added difficulty.

The second JavaScript file is called Types.js. This file will simply store the types of elements we have in the scene.

Right now, that just means the tile:

export const TILE = "tile"
Enter fullscreen mode Exit fullscreen mode

The next two components are strategic — mainly because of their names.

Now, we need both a tower and a tile. Let’s start with Tile.js:

import React, { Component } from "react";
import { DragSource } from "react-dnd";

import Constants from "../constants/Constants";
import { TILE } from "../constants/Types";

const tile = {
    beginDrag({ position }) {
        return { position };
    }
};

const collect = (connect, monitor) => ({
    dragSource: connect.dragSource(),
    dragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
});

class Tile extends Component {
    render() {
        const { position, dragSource, isDragging } = this.props;
        const display = isDragging ? "none" : "block";
        const opacity = isDragging ? 0.5 : 1;
        const width = `(${Constants.TOWER_WIDTH} + ${position * 100}px)`;
        const offset = `${(position * Constants.TILE_WIDTH_BASE) / 2}vw`;

        const tileStyle = {
            display: display,
            opacity: opacity,
            height: "60px",
            width: `calc(${width})`,
            transform: `translateX(calc(${offset} * -1))`,
            border: "4px solid white",
            borderRadius: "10px",
            background: "#764abc"
        };

        return dragSource(<div style={tileStyle} position={position} />);
    }
}

export default DragSource(TILE, tile, collect)(Tile);
Enter fullscreen mode Exit fullscreen mode

Tile is the first high-order component that represents our drag element (DragSource). We drag tiles into towers.

Note that by the end of the code, our DragSource declaration needs some arguments:

  • the first is related to the type of the source (a string constant)
  • the second relates to the source contract implementation, which is made of two functions:
    • beginDrag: the only required function, which returns the data describing the dragged item
    • endDrag: an optional function, which is called at the end of the drag operation
  • the third is the collecting function, the one responsible for specifying which props to inject into your component, as well as managing the current drag state

The rest of the implementation is style-related. It applies our CSS style to the tile component.

Now let’s get to our Tower.js code. Place the following to the file:

import React, { Component } from "react";
import { DropTarget } from "react-dnd";

import Tile from "./Tile";
import Constants from "../constants/Constants";
import { TILE } from "../constants/Types";

const towerTarget = {
    canDrop({ isMoveValid, isTheLatter }, monitor) {
        const isOver = monitor.isOver();
        const position = monitor.getItem().position;
        const tileIsTheLatter = isTheLatter(position);
    const target = parseInt(monitor.targetId.substr(1)) + 1;

        return isOver && tileIsTheLatter ? isMoveValid(position, target) : false;
    },

    drop({ removeTile, addTile }, monitor) {
        const position = monitor.getItem().position;
        const target = parseInt(monitor.targetId.substr(1)) + 1;
        removeTile(position);
        addTile(position, target);
    }
};

const collect = (connect, monitor) => ({
    dropTarget: connect.dropTarget(),
    canDrop: monitor.canDrop(),
    isOver: monitor.isOver()
});

class Tower extends Component {
    render() {
        const background = this.props.isOver ? `#800` : `#764abc`;
        const style = {
      height: `calc(${Constants.TOWER_HEIGHT})`,
      border: "4px solid white",
            borderRadius: "20px 20px 0 0",
            display: "grid",
            alignContent: "flex-end",
            background: background
        };

        return this.props.dropTarget(
            <div style={style}>
                {this.props.tiles && this.props.tiles.map(tile => <Tile key={tile.id} position={tile.id} />)}
            </div>
        );
    }
}

export default DropTarget(TILE, towerTarget, collect)(Tower);
Enter fullscreen mode Exit fullscreen mode

The drop target — DropTarget — class, is pretty similar to the drag source in which concerns the contract and signature.

The first function, canDrop, checks for the boolean value of whether the current operation of dropping is allowed or not.

Three conditions must be met here:

  • the drag source must be over the drop target
  • the tile being moved must be the latter (the top one) of the tower
  • and the move must be true in its validation function (to be created later in App.js).

The drop function, in turn, will take care of removing the current tile from the tower it was placed at, and then add the same to the new tower.

The implementation of these functions will be made in the App.js file since we need these operations to be performed in the same place the state is.

The last file to be created under this folder is the HanoiTower.js:

import React, { Component, Fragment } from "react";
import Tower from "./Tower";
import Constants from "../constants/Constants";

class HanoiTower extends Component {
    render() {
        return (
            <div style={style}>
                {this.props.towers.map(curr => {
                    return (
                        <Fragment key={curr.id}>
                            <div />
                            <Tower
                                tiles={curr.tiles}
                                removeTile={tileId => this.props.removeTile(tileId)}
                                addTile={(tileId, towerId) =>
                                    this.props.addTile(tileId, towerId)
                                }
                                isMoveValid={(tileId, towerId) =>
                                    this.props.isMoveValid(tileId, towerId)
                                }
                                isTheLatter={tileId => this.props.isTheLatter(tileId)}
                            />
                        </Fragment>
                    );
                })}
            </div>
        );
    }
}

const style = {
    height: Constants.HANOI_HEIGHT,
    display: "grid",
    gridTemplateColumns: `
    1fr 
    ${Constants.TOWER_WIDTH}
    2fr
    ${Constants.TOWER_WIDTH}
    2fr
    ${Constants.TOWER_WIDTH}
    1fr
  `,
    alignItems: "flex-end"
};

export default HanoiTower;
Enter fullscreen mode Exit fullscreen mode

This class represents the root component of the game. After App.js, this component will aggregate the other inner component calls.

It places the grid styled nature of the game into the main div that constitutes it.

See that we’re iterating over the array of towers that comes from the main state (to be created).

Depending on how many towers we have there, this will be the number of piles that will be placed on the game screen.

The rest of the code is the style of the component itself.

The next two components are simply structural.

They will determine how the header and footer will appear in the game.

It’s just to make things more beautiful and organized. Here we have the code for Header.js (inside of structure folder):

import React, { Component } from "react";

class Header extends Component {
    render() {
        return (
            <header
                style={{
                    display: "flex",
                    justifyContent: "center",
                    alignItems: "flex-end"
                }}
            >
                <h1
                    style={{
                        color: "#764abc",
                        fontSize: "3em",
                        fontWeight: "bold",
                        textShadow: "2px 2px 2px black"
                    }}
                >
                    THE TOWER OF HANOI
                </h1>
            </header>
        );
    }
}

export default Header;
Enter fullscreen mode Exit fullscreen mode

That’s just styled-component CSS configs. Nothing more.

Here’s the code for Footer.js:

import React, { Component } from "react";

class Footer extends Component {
    render() {
        const defaultStyle = {
            color: "#764abc",
            fontWeight: "bold"
        };

        return (
            <footer
                style={{
                    padding: "0.5em",
                    display: "flex",
                    justifyContent: "space-between",
                    alignItems: "center",
                    fontSize: "14px",
                    backgroundColor: "white"
                }}
            >
                <p>
                    <span style={defaultStyle}>React-DND Example</span>
                </p>
                <p>
                    <span style={defaultStyle}>LogRocket</span>
                </p>
            </footer>
        );
    }
}

export default Footer;
Enter fullscreen mode Exit fullscreen mode

Feel free to customize these components as much as you want.

Finally, let’s analyze the code of our App.js file.

In order to get our previous configured drag and drop components working, we need to provide a DnDProvider that encapsulates the rest of the DnD code.

import React, { Component } from "react";
import HanoiTower from "./hanoi/HanoiTower";
import Header from "./structure/Header";
import Footer from "./structure/Footer";
import Constants from "./constants/Constants";

import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";

class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            towers: [
                { id: 1, tiles: [] },
                { id: 2, tiles: [] },
                { id: 3, tiles: [] }
            ]
        };
    }

    componentDidMount = () => {
        const tiles = [];
        for (let id = 1; id <= Constants.NUM_TILES; id++) {
            tiles.push({ id: id });
        }

        this.setState({
            towers: [
                { id: 1, tiles: tiles },
                { id: 2, tiles: [] },
                { id: 3, tiles: [] }
            ]
        });
    };

    removeTile = tileId => {
        var towerId = null;
        this.setState(prevState => {
            prevState.towers.forEach(tower => {
                tower.tiles = tower.tiles.filter(tile => {
                    if (tile.id === tileId) {
                        towerId = tower.id;
                        return false;
                    } else {
                        return true;
                    }
                });
            });

            return {
                towers: prevState.towers
            };
        });
        return towerId;
    };

    addTile = (tileId, towerId) => {
        this.setState(prevState => ({
            towers: prevState.towers.map(tower => {
                tower.id === towerId && tower.tiles.unshift({ id: tileId });

                return tower;
            })
        }));
    };

    isMoveValid = (tileId, towerId) => {
        var tower = this.state.towers[towerId - 1];
        if (tower.tiles.length === 0 || tileId < tower.tiles[0].id) {
            return true;
        } else if (tileId > tower.tiles[0].id || tileId === tower.tiles[0].id) {
            return false;
        }
    };

    isTheLatter = tileId => {
        let tileIsTheLatter = false;
        this.state.towers.forEach(tower => {
            if (tower.tiles.length !== 0 && tower.tiles[0].id === tileId) {
                tileIsTheLatter = true;
            }
        });
        return tileIsTheLatter;
    };

    isVictory = () => {
        const { towers } = this.state;
        return (
            towers[1].tiles.length === Constants.NUM_TILES ||
            towers[2].tiles.length === Constants.NUM_TILES
        );
    };

    render() {
        return (
            <div style={layoutStyle}>
                <DndProvider backend={HTML5Backend}>
                    <Header />
                    <HanoiTower
                        towers={this.state.towers}
                        removeTile={this.removeTile}
                        addTile={this.addTile}
                        isMoveValid={this.isMoveValid}
                        isTheLatter={this.isTheLatter}
                    />
                    {this.isVictory() && alert("Victory!")}
                    <Footer />
                </DndProvider>
            </div>
        );
    }
}

const layoutStyle = {
    display: "grid",
    gridTemplateRows: `
    ${Constants.HEADER_HEIGHT}
    calc(${Constants.HANOI_HEIGHT})
    ${Constants.FOOTER_HEIGHT}
  `
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Let’s break some things down.

The first important thing to note is the constructor.

It places our state and — since we’re not using Redux or any other state management lib — we’ll use the old React way to manipulate state values via props being passed down the components hierarchy.

Our towers array will consist of only three elements (remember to change the Constants class if you want to increase this value).

As soon as the component mounts, we need to initiate our array with the tiles stack within the first tower.

The componentDidMount function will take care of this.

Then, we have the auxiliary functions our inner components will use:

removeTile

Sets the new state by iterating over our towers array and searching for the corresponding tile id (passed as param).

addTile

Sets the new state by adding the passed to the tiles array of the respective tower selected, via unshift function (it adds the value to the beginning of the array).

isMoveValid

Checks for basic rules of the game, such as whether a player is attempting to drop a smaller tile over a larger tile, etc.

isVictory

Checks for the conditions over the current state’s towers array to see whether the player has won the game or not.

The end of the code just uses the imported DnDProvider, passing the HTML5Backend as the backend for the provider.

Note also that every time this component re-renders, we check for the isVictory function to see if an alert message must be shown.

What’s missing is just the background image we’re using for the game (you can download it via GitHub project link, available at the end of the article); and the style.css code:

html,
body {
    margin: 0;
    padding: 0;
    border: 0;
    font-family: "Press Start 2P", sans-serif;
    background-image: url(../img/bg.gif);
    background-size: cover;
    background-repeat: no-repeat;
}
Enter fullscreen mode Exit fullscreen mode

Plus, don’t forget to import the style.css file in your index.js file:

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import "./style/style.css";

import * as serviceWorker from './serviceWorker';

const mountNode = document.getElementById("root");

ReactDOM.render(<App />, mountNode);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

Conclusion

That’s it. You can access the full source code here at GitHub.

In this tutorial, we’ve configured and learned a bit more about how this powerful lib works.

Again, I can’t stress enough how important it is to get a closer look at the official documentation.

You can improve the game by adding some menus, a time counter to challenge the users, an option that allows users to input how many tiles they want to play with at the beginning of the game.

Regarding react-dnd, there are many more examples on their official website that you can use when looking for new functionalities in your application.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post Using React-dnd to create a Tower of Hanoi game appeared first on LogRocket Blog.

Top comments (0)