javascriptsliding-tile-puzzle

How to check if a block is movable and swap it with the empty block in JS puzzle


I want to move a block to the empty space (swap the block with the empty block) when clicked.

But before doing so, I have to verify if the block is movable.

I'm confused with how to apply the logic here. I've seen many solutions but I don't really understand and others used jQuery plus I don't still get it :-/

I need some help please.

Here's what I tried doing:

randomOrder.forEach((el, i) => {
        const block = document.createElement('div')
        block.classList.add('block')
        block.id = "b" + el
        block.style.left = (4 + 98 * (i % 3)) + 'px'
        block.style.top = (4 + 98 * Math.floor(i / 3)) + 'px'
        block.innerText = el == 9 ? "" : el
        if(el == 9){ freeBlock = block }
        container.appendChild(block)
    }) //////////////

    // get clicked block
    container.addEventListener("click", (e) => {
        var clickedBlock = e.target
        if (e.target.id === "b9") {
            return 0
        }
        else {
            moveBlock(clickedBlock)
        }
    }) ///////////////

    // Function To Move Block
    function moveBlock(clickedBlock) {
        // check if the block is movable before moving it
        if (isMovable(clickedBlock)) {
            console.log(clickedBlock.offsetLeft)
        }
    } /////////////// end moveBlock()

    // check if block is movable
    function isMovable(clickedBlock) {
        if(clickedBlock.offsetLeft === freeBlock.offsetLeft){
            if(clickedBlock.offsetTop === freeBlock.offsetTop){
                return true
            }
        }
    } //////////// end check if movable

Anyone knows how I can do this ?


Solution

  • I would first change two things in your setup:

    1. Don't create a block in the DOM for value 9. Just only make blocks for the other values (8 blocks)

    2. Keep the array from which you created the blocks updated with every move. It is good practice to keep the state of the game in variables and treat the display as a sort of side effect.

    Here is how you could do it:

    const container = document.querySelector(".container");
    const state = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    function moveBlockTo(block, i) {
        block.style.left = (4 + 98*(i % 3)) + 'px';
        block.style.top = (4 + 98*Math.floor(i / 3)) + 'px';
    }
    
    state.forEach((el, i) => {
        if (el == 9) return; // No block is created for 9
        const block = document.createElement('div');
        block.classList.add('block');
        moveBlockTo(block, i);
        block.innerText = el;
        container.appendChild(block);
    });
    
    container.addEventListener("click", e => {
        const block = e.target;
        if (!block.classList.contains('block')) return; // Click was not on a block
        // Get the position of the block as an index in the array
        const i = state.indexOf(+block.innerText);
        const gap = state.indexOf(9);
        // Check that the gap is neighboring the clicked tile
        if (i % 3 > 0 && i - 1 === gap || 
                i % 3 < 2 && i + 1 === gap ||
                i - 3 === gap ||
                i + 3 === gap) {
            // It is a valid move, so now swap the gap with the clicked tile
            // 1. Adapt state to reflect the move
            [state[i], state[gap]] = [state[gap], state[i]];
            // 2. Move the block in the gap
            moveBlockTo(block, gap);
        }
    });
    .container{
        width: 300px;
        padding: 3px;
        aspect-ratio: 1;
        position: relative;
        margin: 100px auto;
        background: #777;
        border-radius: 10px;
        box-shadow: 0 0 0 5px #000;
    }
    
    .block{
        width: 31%;
        height: 31%;
        margin: 1.5px;
        display: flex;
        font-size: 3rem;
        cursor: pointer;
        user-select: none;
        background: #eee;
        position: absolute;
        border-radius: 10px;
        align-items: center;
        justify-content: center;
        border: 2px solid #000;    
        box-shadow: 0 0 20px #555 inset;
    }
    <div class="container"></div>