Build an 8 Puzzle Game With Pure JavaScript

Build an 8 Puzzle Game With Pure JavaScript

Hey yo! In this post we are going to be creating a game with basic web design skills. All you need is basic knowledge of html, css and JavaScript. The game we are making is the popular 8 Puzzle Game. It is made of 9 tiles with 8 of them filled up with contents and the last one is empty. To solve the puzzle, the player will have to rearrange the tiles.

Open up your favorite text editor, I am using vscode and create the following files: folder structure.PNG

Now we create a div container with an unordered list having 9 list elements.

/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="css/style.css">
    <title>Sliding Tile Puzzle</title>
</head>

<body>
    <div id="container">
        <ul>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>
    </div>

</body>
<script src="js/script.js"></script>

</html>

Now, let's add some styling. First we remove default padding and margin by selecting every element on the page by using the *. I also made the body a flex container so that we can use flexbox to style the page. The container div is given a height and width of 500px and a nice background that goes along with the body's background. This line margin: 10px auto; centers container div by giving it a 10px margin at the top and bottom and an "automatic" (equal) margin on the left and right.

/css/style.css

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

body {
    display: flex;
    background-color: #114B5F;
    text-align: center;
    flex-direction: column;
    padding-top: 2%;

}

#container {
    width: 500px;
    height: 500px;
    background: #E4FDE1;
    margin: 10px auto;
}

Preview index.html in your favorite browser, you should have this too:

preview_1.PNG

Now, let's style the list items. I will make the ul a flex container too and space the list items evenly:

/css/style.css

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

body {
    display: flex;
    background-color: #114B5F;
    text-align: center;
    flex-direction: column;
    padding-top: 2%;

}

#container {
    width: 500px;
    height: 500px;
    background: #E4FDE1;
    margin: 10px auto;
}

ul {
    display: flex;
    flex-wrap: wrap;
    list-style-type: none;
    justify-content: space-evenly;
    font-family: 'Audiowide';
    color: #E4FDE1;
}
ul li {
    background: #456990;
    width: 30%;
    height: 150px;
    border: 1px solid #028090;
    margin-top: 10px;
    text-align: center;
    font-size: 2rem;
    padding-top: 3rem;
}

Preview the index.html again, you should have something like this: preview_2.PNG

Nice.

In the 8 tile sliding puzzle, the 9th tile should be empty so we will create a class that styles the empty tile. Before then, let's give the list items contents, update index.html to have the following content (I've included the link for the Audiowide font);

/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="css/style.css">
    <link href='https://fonts.googleapis.com/css?family=Audiowide' rel='stylesheet'>
    <title>Sliding Tile Puzzle</title>
</head>

<body>
    <div id="container">
        <ul>
            <li>A</li>
            <li>B</li>
            <li>C</li>
            <li>D</li>
            <li>E</li>
            <li>F</li>
            <li>G</li>
            <li>H</li>
            <li class="empty"></li>
        </ul>
    </div>

</body>
<script src="js/script.js"></script>

</html>

Add the following to the style sheet to style the empty tile:

/css/style.css

...
.empty {
    background: #DAD4EF;
    border: 2px solid #114B5F;
}
...

Preview again:

preview_3.PNG

Let's add a nice heading before the container:

/index.html

...
<body>
    <h1>Sliding Tile Puzzle</h1>
    <div id="container">
        <ul>
            <li>A</li>
            <li>B</li>
...

The following is the styles for the heading:

/css/style.css

h1 {
    color: #E4FDE1;
    font-family: 'Sofia';
    text-align: center;

}

I have included a link to the Sofia font in the head section:

/index.html

...
<link href='https://fonts.googleapis.com/css?family=Sofia' rel='stylesheet'>
    <link href='https://fonts.googleapis.com/css?family=Audiowide' rel='stylesheet'>
    <title>Sliding Tile Puzzle</title>
...

Preview the webpage again, you should have this:

preview_4.PNG

The design of the game is complete, now let's give it life with JavaScript! Boomshakala!

Implementing functionality with JavaScript

To play the game, players will have to drag and drop tiles into available empty tiles. We are going to use the HTML Drag and Drop API .

This api let's make some elements draggable and some droppable, to do this we have to define drag and drop event handlers in the script file. Open up the script.js and fill it up with the following contents:

/js/script.js

const dragstart_handler = ev => {
    console.log("dragstart")
    ev.dataTransfer.setData("text/plain", ev.target.id)
    ev.dataTransfer.dropEffect = "move";
}

The dragstart_handler handles the event ev emitted when the user starts dragging. Because we will be transferring data while dragging, we get the data attached to the element and add it to the dataTransfer object, which is an object used to hold the data that is being dragged during a drag and drop operation.

Update the script.js with the following content: /js/script.js/

...
const dragover_handler = ev => {
    console.log("dragOver");
    ev.preventDefault();
}

const drop_handler = ev => {
    console.log("drag")
    ev.preventDefault();
    // Get the id of the target and add the moved element to the target's DOM
    const data = ev.dataTransfer.getData("text/plain");
    ev.target.innerText = document.getElementById(data).innerText;
}

const dragend_handler = ev => {
  console.log("dragEnd");
  // Remove all of the drag data
  ev.dataTransfer.clearData();
}

The dragover_handler event handler is a simple function, the only important operation we perform with it is to call preventDefault() to prevent additional event processing for this event. We log out the event so that we can keep track of when it is fired.

In the drop_handler event handler, we get the data stored in the dataTransfer object and then we 'transfer' it to the element in which we are dropping unto (ev.target.innerText = document.getElementById(data).innerText;).

In the dragend_handler, we clear the data stored in the dataTransfer when the drag event ends.

Now, we need to update the index.html file, we will make the list item before the empty tile ('H' tile) draggable and make the empty tile droppable.

/index.html

...
<ul>
            <li>A</li>
            <li>B</li>
            <li>C</li>
            <li>D</li>
            <li>E</li>
            <li>F</li>
            <li>G</li>
            <li id="li8" draggable="true" ondragstart="dragstart_handler(event)" ondragend="dragend_handler(event)">H</li>
            <li id="li9" class="empty" ondrop="drop_handler(event);" ondragover="dragover_handler(event);"></li>
        </ul>
...

I gave both list items id's since in our event handlers, we had to get their content via their id's (see the dragstart_handler where we had ev.dataTransfer.setData("text/plain", ev.target.id)).

The 'H' tile is made draggable and I also set it's ondragstart and ondragend attributes to the relevant event handlers. The empty tile is madedroppable and it now has attributes for ondrop and ondragover event handling.

Preview the webpage, drag and drop the 'H' tile unto the empty tile:

preview_drag_1.PNG The drag and drop feature works, but there is a problem. After dropping the content of the 'H' tile, we now have two tiles having 'H' in them. We will need to empty the first tile after the drop operation. Later, we will change the styling of the tiles after drag and drop operation.

To empty the source tile after a drop operation, update the script file as follows:

/js/script.js

...
const drop_handler = ev => {
    console.log("drag")
    ev.preventDefault();
    // Get the id of the target and add the moved element to the target's DOM
    const data = ev.dataTransfer.getData("text/plain");
    ev.target.innerText = document.getElementById(data).innerText;
    document.getElementById(data).innerText = "";
}
...

Moving all functionality to JavaScript

The tiles currently have contents that are defined in the html file, we will remove those contents and fill them in with JavaScript. Update the index.html file as follows:

/index.html

...
<ul>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
            <li></li>
        </ul>
...

In script.js type in the following:

js/script.js

// select the list items
let ul = document.querySelectorAll('li');;
const letters= ["A", "B", "C", "D", "E", "F", "G", "H", ""]

// this function sets a unique id for each list item, in the form 'li0' to 'li8'
const setId = (items) => {
    for(let i = 0; i < items.length; i++) {
        items[i].setAttribute("id", `li${i}`)
    }
}

const fillGrid = (items, letters) => {
    items.forEach((item, i) => {
        item.innerText = letters[i];
    })
}

fillGrid(ul, letters);

First I got all the li items into a node list (something that is like an array) using the querySelectorAll. The letters array has the list of letters and the last item is the empty string, this is for the empty tile.

In the setId function, I set ids for each list item by calling the setAttribute() function on each of them, the id is made by concantenating the prefix li with the index of the for loop.

In the fillGrid function, I loop through all the li items while keeping track of their index, i, and I fill in each list item with an element from the letters array, accessing it with the index i.

If you preview the web page, you have the tiles filled up as we had earlier, but now we are doing that from JavaScript. Cool :) (Don't worry about the display of the empty tile yet)

Randomizing the tiles

The tiles are filled up with the contents of the letters which are from A-H, with the present script we will always have the same configuration of the tiles. What we want is that the tiles should be shuffled up so that we always get a random configuration that must then be rearranged to solve the puzzle.

Shuffle the letters arrays before filling in.

How can we write a function that will shuffle the array? The algorithm for this function is as follows

  • Loop through the array
  • For each index i in the loop, pick another random index j
  • swap elements at index i and j

Stop!!! Try to implement this algorithm before seeing my implementation.

js/script.js

...
// shuffle the array
const shuffle = (arr) => {
    const copy = [...arr];
    // loop over the array
    for(let i = 0; i < copy.length; i++) {
        // for each index,i pick a random index j 
        let j = parseInt(Math.random()*copy.length);
        // swap elements at i and j
        let temp = copy[i];
        copy[i] = copy[j];
        copy[j] = temp;
    }   
    return copy;
 }
...

Did you come up with something similar?

Note that I made a copy of the array in the shuffle function before shuffling it, this makes sure that we do not change (a.k.a mutate) the passed in array but return a new shuffled one.

Now, update the fillGrid function to first shuffle the array before filling in the tiles:

/js/script.js

...
const fillGrid = (items, letters) => {
    let shuffled = shuffle(letters);

    items.forEach((item, i) => {
        item.innerText = shuffled[i];
    })
}
...

Now when you refresh the page, you get a new configuration of the tile each time. Boomshakala!!!

Setting Droppable and Dragabble Tiles

Now, we will write a function that will take in the list of li items, find the one whose content is empty and make it droppable.

We will also write a function that will make all the tiles on the page draggable, we will change this function later so that it's only tiles that are top, bottom, left and right of the empty tile that will be draggable.

Before these functions, we will create a setUp function, from where we will make all the function calls. Move all the function calls that we presently have into the setUp function.

/js/script


// select the list items
let ul = document.querySelectorAll('li');;
const letters= ["A", "B", "C", "D", "E", "F", "G", "H", ""]

function setUp() {
    fillGrid(ul, letters);
    setId(ul)
    // set up the droppable and dragabble contents
    setDroppable(ul) ;
    setDraggable(ul);

}

const state = {}
state.content = letters;

/**
 * setters
*/
const setDroppable = (items) => {
    items.forEach((item, i) => {
        if(!item.innerText) {
            item.setAttribute("ondrop", "drop_handler(event);");
            item.setAttribute("ondragover", "dragover_handler(event);");
            item.setAttribute("class", "empty");
        }
        return;
    })
}

const setDraggable = (items) => {
    items.forEach(item => {
            item.setAttribute("draggable", "true");
            item.setAttribute("ondragstart", "dragstart_handler(event)");
            item.setAttribute("ondragend", "dragend_handler(event)");
    })
}
...

Notice that I have created a state object that will hold the state of the game. Update the index.html file so that when the body loads, the setUp function is called:

/index.html

...
<body onload="setUp()">
    <h1>Sliding Tile Puzzle</h1>
...

If you preview the page, you will notice that you can drop tiles unto the empty tile but when you do, the tile is filled up but it's still styled as the empty tile and its the only tile that you can drop tiles.

We will now update the app so that, when an empty tile is dropped unto from a source tile, it becomes 'filled up' and it's no more droppable. Remember that we used the setAttribute(key, value) function to set an attribute, we will use the same function to remove the attribute, we will set the value of the attribute we want to remove to an empty string. We will then make the source tile droppable and empty:

/js/script.js

...
const drop_handler = ev => {
    console.log("drag")
    ev.preventDefault();
    // Get the id of the target and add the moved element to the target's DOM
    const data = ev.dataTransfer.getData("text/plain");
    ev.target.innerText = document.getElementById(data).innerText;

    // once dropped, unempty the cell :)
    ev.target.classList.remove("empty")
    // remove relevant attributes
    ev.target.setAttribute("ondrop", "");
    ev.target.setAttribute("ondragover", "");
    document.getElementById(data).innerText = "";
}

const dragend_handler = ev => {
  console.log("dragEnd");
  // Remove all of the drag data
  ev.dataTransfer.clearData();

  // set new droppable and draggable attributes
  setDroppable(document.querySelectorAll('li'));
  setDraggable(document.querySelectorAll('li'))
}
...

Test out the app now and you will find that everytime you drag and drop a tile, the source becomes an empty tile and the destination becomes filled up!. You will also notice that you can drag and drop tiles that are not adjacent to the empty tile, later we will fix this.

Getting the game state and dimension

Each time a player drag and drops a tile, the arrangement of the tiles changes, we want to keep track of this change in the state object. Let's create a function that gets the state. We will also make a function that will store the visual representation of the tiles as an array of arrays. Update the script file as follows:

/js/script.js

...
function setUp() {
    fillGrid(ul, letters);
    setId(ul)

    state.content = getState(ul);
    state.dimension = getDimension(state);

 // set up the droppable and dragabble contents
    setDroppable(ul) ;
    setDraggable(ul);
    console.log("The state dimension", state.dimension)

}

const state = {}
state.content = letters;


/**
 * Getters
 */
const getState = (items) => {
    const content = [];
    items.forEach((item, i) => {
        content.push(item.innerText)
    });
    return content;
}

const getDimension = (state) => {
    let j = 0;
    let arr = [];
    const {content} = state;
    for(let i = 0; i < 3; i++) {
        arr.push(content.slice(j, j+3));
        j+=3;
    }

    return arr;
}

/**
 * setters
*/
const setDroppable = (items) => {
    items.forEach((item, i) => {
        if(!item.innerText) {
            state.emptyCellIndex = i;
            item.setAttribute("ondrop", "drop_handler(event);");
            item.setAttribute("ondragover", "dragover_handler(event);");
            item.setAttribute("class", "empty");
        }
        return;
    })
}
...

The getDimension function loops through the content (which is an array of the letters with length 9) three times, each time it slices three contents and puts them in an array, this gives us an array of 3 arrays. The first array represents the first row of the game, the second array represents the second row and the third array represents the third row.

This means that we now have the game state stored in two formats: state.content stores the tiles in a linear sequence (1D array); state.dimension stores the tiles in a 2D array as its visual representation.

preview_dimension.PNG state.content vs state.dimension

In the setUp function, I logged out the output of the getDimension function, you will see that it stores the visual representation of the game in a nice 2D array. Notice that I added a new line to the setDroppable function to store the index of the empty tile in the app state, state.emptyCellIndex = i; The index stored here is the index from the 1D linear array of li items.

content_dimension.PNG

Getting the empty cell

We need to get the empty cell so that we can make only its adjacent tiles dragabble. Presently we can drag any tile unto the empty tile, see the image below. In each picture only the dotted tiles (in red) should be draggable:

draggable.png

We will create a getEmptyCell function that will return the 2D positions of the empty cell from the state.dimension. We already have the index of the empty cell stored in the state.emptyCellIndex which stores the 1D index of the empty cell.

How to get the 2D positions from the 1D index

We will do some math in order to compute the 2D positions of the empty cell from the 1D index. The 2D representation of the game has 3 rows and 3 columns; it consist of 9 tiles in total. The state.emptyCellIndex contains the linear index of the empty cell, since indices of arrays start from 0, the actual cell number of the empty array will be state.emptyCellIndex+1.

To get the row in which this falls, we will divide the cell number by 3, (the number of cells in a row). We take the ceil of the result, this is the row number in which the empty falls.

Getting the column in which the empty cell falls is quite tricky; we multiply the emptyCellRow number by 3 and subract the emptyCellNumber from the result, we then subtract the total result from 3. Create a getEmptyCell function right after the getState function: /js/script.js

...
const getEmptyCell = () => {
    const emptyCellNumber = state.emptyCellIndex+1;
    const emptyCellRow = Math.ceil(emptyCellNumber/3);
    const emptyCellCol = 3 - (3 * emptyCellRow - emptyCellNumber);
    // emptyCellRow holds the actual row number the empty tile falls into in a 9-cell grid
    // the array index will be one less than its value. Same goes for emptyCellCol
    return [emptyCellRow-1, emptyCellCol-1]
}

Making only adjacent tiles to empty cell dragabble

Now that we have a way to get the position of the empty cell, we can make only adjacent cells around it draggable. An empty cell can have a cell above, below , to the left or to the right of it that should be draggable. An empty cell may sometimes not have all 4 adjacent sides:

empty_tiles_adjacent.png How can we get the indices of the tiles adjacent to the empty tile? Let's take an example:

row_col.png

In the example above, the empty tile is on row 1 and col 1. The tiles on the left and right of it are both on the same row 1. The left tile is on col 0 ( row of empty tile - 1). The right tile is on col 2 (row of empty tile + 1). Similarly, the top and bottom tiles are on the same col 1 as the empty tile but on row 0 and row 2 respectively. Let's update the setDraggable function:

/js/script.js

...
const setDraggable = (items) => {
    const [row, col] = getEmptyCell();

    let left, right, top, bottom = null;
    if(state.dimension[row][col-1]) left = state.dimension[row][col-1];
    if(state.dimension[row][col+1]) right = state.dimension[row][col+1];

    if(state.dimension[row-1] != undefined) top = state.dimension[row-1][col];
    if(state.dimension[row+1] != undefined) bottom = state.dimension[row+1][col];


    // make its right and left dragabble
    items.forEach(item => {
        if(item.innerText == top || 
            item.innerText == bottom || 
            item.innerText == right ||
            item.innerText == left) {
                item.setAttribute("draggable", "true");
                item.setAttribute("ondragstart", "dragstart_handler(event)");
                item.setAttribute("ondragend", "dragend_handler(event)")
            }

    })
}
...

Now, we get the adjacent tiles and make only those tiles dragbble. Test the game, you will notice that only the tiles adjacent to the empty tile are draggable. If you tested the game enough, you will notice that some tiles that have been made draggable when an empty tile was adjacent to them are still draggable after the empty file as been filled up.

How can we fix this? Simple: We need to update the app state and dimension every time we complete a drag and drop. Update the script file as follows:

/js/script.js

...
const drop_handler = ev => {
    console.log("drag")
    ev.preventDefault();
    // Get the id of the target and add the moved element to the target's DOM
    const data = ev.dataTransfer.getData("text/plain");
    ev.target.innerText = document.getElementById(data).innerText;

    // once dropped, unempty the cell :)
    ev.target.classList.remove("empty")
    ev.target.setAttribute("ondrop", "");
    ev.target.setAttribute("ondragover", "");
    document.getElementById(data).innerText = "";

    // get new state after dropping
    state.content = getState(ul);
    // get new dimention from the state after dropping
    state.dimension = getDimension(state);
}
...

There is still a subtle problem that is similar to the one we just solved. All the tiles that we made droppable when they were empty are still droppable even when they become filled up, we will now create a function (called removeDroppable) that will make a tile undroppable once it is filled up.

We will call this function in the dragend_handler function. Create a removeDroppable function after the setDroppable function and call it in the dragend_handler function as follows:

/js/script.js

...
const removeDroppable = (items) => {
    items.forEach((item) => {
        item.setAttribute("ondrop", "");
        item.setAttribute("ondragover", "");
        item.setAttribute("draggable", "false");
        item.setAttribute("ondragstart", "");
        item.setAttribute("ondragend", "");
    })
}

...

const dragend_handler = ev => {
  console.log("dragEnd");
  // Remove all of the drag data
  ev.dataTransfer.clearData();
  // remove all droppable attributes
  removeDroppable(document.querySelectorAll('li'));

  // set new droppable and draggable attributes
  setDroppable(document.querySelectorAll('li'));
  setDraggable(document.querySelectorAll('li'))
}
...

Unto the final steps

The game is almost complete, we now have to check if the user has arranged the tiles in the right order. Before then, there is an important property of 8-puzzle tile that we need to consider: it's not all configurations that are solvable.

Since we are randomizing the tiles, it's possible to get a configuration that is not solvable.How do we tell if a given instance of the puzzle is solvable?

A given instance of 8 puzzle is solvable only if the number of inversions is an even number in the configuration.

What's an inversion? An inversion occurs when a pair of tiles are in the reverse order as they should be in the correct configuration. For example, given the following configuration:

[
 ["A", "B", "C"],
 ["D", "", "E"],
 ["H", "F",  "G"]
]

There are two inversions in the configuration (H, F) and (H, G), since the number of inversions is two (an even number), this configuration will be solvable. To know if a given configuration is solvable, we count the number of inversions. How do we do that?

Simple: We pick each element and compare it with other elements after it, if the first element is 'greater' than the second element, then we have encountered an inversion. Open up the script.js file and create an inSolvable function right before the fillGrid function, also we we will update the fillGrid function to only fill the tiles if the generated configuration is solvable:

/js/script.js

...
const isSolvable = (arr) => {
    let number_of_inv = 0;
    // get the number of inversions
    for(let i =0; i<arr.length; i++){
        // i picks the first element
        for(let j = i+1; j < arr.length; j++) {
            // check that an element exist at index i and j, then check that element at i > at j
            if((arr[i] && arr[j]) && arr[i] > arr[j]) number_of_inv++;
        }
    }
    // if the number of inversions is even
    // the puzzle is solvable
    return (number_of_inv % 2 == 0);
}

const fillGrid = (items, letters) => {
    let shuffled = shuffle(letters);
    // shuffle the letters arraay until there is a combination that is solvable
    while(!isSolvable(shuffled)) {
        shuffled = shuffle(letters);
    }

    items.forEach((item, i) => {
        item.innerText = shuffled[i];
    })
}
...

Now we are sure that the configuration will always be solvable!

Checking for the correct solution

We now have to check when a player has solved the puzzle. We will create an isCorrect function that will take in state.content and letters arrays, convert both of them to a string, and then compare both strings to check if they are equal. Create the function after the isSolvable function:

/js/script.js

...
const isCorrect = (solution, content) => {
    if(JSON.stringify(solution) == JSON.stringify(content)) return true;
    return false;
}
...

When do we check for the solution? At the end of every drag event, so we have to update the dragend_handler function:

...
const dragend_handler = ev => {
  console.log("dragEnd");
  // Remove all of the drag data
  ev.dataTransfer.clearData();
  // remove all droppable attributes
  removeDroppable(document.querySelectorAll('li'));
  // set new droppable and draggable attributes
  setDroppable(document.querySelectorAll('li'));
  setDraggable(document.querySelectorAll('li'))

  // if correct
  if(isCorrect(letters, state.content)) {
      console.log("Smart Yo!. You win");
  }
}
...

Try the game out, when the arrangement is correct, the message is logged to the console. We wouldn't want players of our game to check the console to know when they solve the puzzle. We will be creating a modal that will pop up when the puzzle as been solved, I will move a bit fast, the codes are easy to comprehend:

/index.html

...
<body onload="setUp()">
    <h1>Sliding Tile Puzzle</h1>
    <div id="modal" class="hide">
        <div id="header"><button id="closeBtn" onclick="hideModal()">x</button></div>
        <h1 id="message">You won!</h1>
    </div>
    <div id="container">
...

css/style.css

...
/* Modal styles */
#modal {
    width: 40%;
    height: 300px;
    background: #DE5360;
    position: absolute;
    top: 30%;
    left: 50%;
    transform: translateX(-50%);
    font-family: 'Sofia';
}

#modal #message {
    color: #FDE1E3;
    margin: 0;
    font-size: 4rem;
}

.hide {
    display: none;
}
#modal #header {
    text-align: right;
    padding: 5px 10px;
}
#modal #header button {
    padding: 10px;
    border: 0px;
    background: #456990;
    color: #E4FDE1;
    cursor: pointer;
}

/js/script.js

const dragend_handler = ev => {
  console.log("dragEnd");
  // Remove all of the drag data
  ev.dataTransfer.clearData();
  // remove all droppable attributes
  removeDroppable(document.querySelectorAll('li'));

  // set new droppable and draggable attributes
  setDroppable(document.querySelectorAll('li'));
  setDraggable(document.querySelectorAll('li'))

    // if correct
    if(isCorrect(letters, state.content)) {
        showModal();
    }
}

const showModal = () => {
    document.getElementById('message').innerText = "You Won!";
    document.getElementById('modal').classList.remove("hide");

}

const hideModal = () => {
    document.getElementById('modal').classList.add("hide");
}

Try out the game now:

you_won.PNG

Congratulations!!!

done.gif Wow! It's been a long ride, I hope you have learnt some new stuff and had fun while building the 8 puzzle game. The complete code is on github here .

Share this post if you find it helpful and you can follow me on twitter @solathecoder for more awesome tech posts.

Until we meet again, happy coding!