Coding a Sliding Puzzle game in Javascript
Tags: javascript, dml, gamedev
Published: 2021-09-10 21:07:30

See the source code below.
You can play with it here : [SlidingPuzzle](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcIB0ALeBbKIANCLAPYB2YpMaJ1UAhgA6QrFiwBO9aA2gAyEAjAE4AzBgBs-Gf0lD+YgKxClkgEwBdYgDMAljDB9Q5Brgi1seAsTLlE92gB4c+AASNyAcwC8AHRAkCACAPj9yNzcXCAYkMIio83gGN1gsBk5IeH8QAFd4HQBaAA5Q8MjIp3g9eBgQ6tqIJwB6BrryiqcklNNzHIA3PQgAdyZSTngA1IoHbIDhvSR4LB8gwdgIQoWlrEI3PXIavQYoQo4TiB8hDH4yhMqOTj0meDcwTlgcnHgWVGbmiA6JgMLwQDBBZoAEQAsgAZKFwgD6WFI5mBoOaUD0ACMEbDCrgDhgAFZgUItR7PeDxSrNLAxOIdJzY0hIACeITc4SZlJenI6Oly5AQegobxgECYbgAFLgwABKNzADqRTgQeC5TgRcgjNwABW4hMg0ulnEIEE4nEVPk5yvuFSyABU9OZSPlTYQ5fKVW4AL7ehK+jodWCMMBgNwAIVIGSQSp9Ptg4YAwtAoG4fG4AAY+yLYhiwADWXm4QqQqGxUFyEEG1HVAG56XovDhUAp+EwAB71tzbZZtmRd+u5ty4DJeA6odRD2CaqicVBjA6ITj1nQwTuoNz8bdudtDkdIPRgJiMNmodcQbsnZvkQo1CByrcbewW4f2yLE3Jgao6NmFOxZmfCBX1XMgoHGLdxgYbwIB7H0c3tRNw2jWMM2zEdgSQI9vFQJQhyPE8zwvDd3wqCpL07LZOGYVBhhoph63zIsSzdchy2g2CyPI3tFn7AAWIRJCHMdOAnchUB3Bh8lIbjIkQ8juXtNI4ELAA1G8kFQzg42lRVgC5D83EFYVqjFFSiz1UgI2lWB9JHVV1U1CJYHBBhkiyDAxgjHxM1cpB3IYTzZ04byRyDIy1Q1LU3GWY8MA2KAoDADAawtNlbPpSzrPlOS-QTe19HYyE9DVEUKEdUhoVIfoIFstN7KMkc7B-NxeFBeBk2oQgOoAJVIYZNHQ3hpW861OW8gBSfjCFGnKM05aF3KwDB4E4IVYDmsBmn4+VNAcxSjJa15Eqgbz0IAOVyXBsQteqkrcjz1QSzUxryyJjrcR8XjZVMkvOy7rtuzhpTilLvvgX600eoLnpCt7DJ4xGeM+7hhnAgBRXAfuGvqBulCGof+nK9g6rqoAJ7HIb+s6cs0d7pkoV40fAmn0LavHhnu2mFVJ9Vye5sb6ZHZqKFasAzAgcmAHlOH64Z2ZHU0Bsxqm2QEIbfLcFnqBpjW3EVAAfQ2ZR1qAsZ+3ghE1zMzb163cpFoy9B0GUAEIJfMGW5YGxUouchnRaZtwjzKszKHQu0kciXhUjTc7Cj3IatwCGAdEmIgDtj07zoAaiTtwU5AJ4Wwzwgs7j4mI0T-jk65PImACcujJjyuebcfPa8L+ukAG8gpnCkcYFebzQ-Q6XsWJOB4AwQsIDZMBpVD6fRUoeUMCKpAtoWtxzq1wmabekd-ZipfSpXsXeFH0rNFy6PkciX0H+fzfpe8h8SrD1ez6-ih9OfyILsZTL3KhELWARciNxAP-E+EQAi92GP3EA+Vnaux-hfMBmZ4F9wCDApyMUIFQJQUjIB6DQEZiwSANOGc8HRTgcXZsOApgRRIWgkB4d0IBBLkw6BSptb4PodQ5hPoWEVB9LgGqUs0wylOo1JGn12Gr0iJmMGG8DhIE-hgyq1Varc0dqg92ii-78LoYHI6YtXhmDYq8TMCh+IMwUZqdySjMzqBkA4ix0xASRwOgAcmob47uhQhDN3vr47h8BAlbhCX4hB5Aol7lCdHXxkCEnBMHk1cxwdSBMHfogTR5DMyxSwPFV+eSIAFPDmQ8O+ieJB1akmMAjoaKUEYIgdC3M9hGPIONeMLd9hsPPoUzM-jASRLcMbEOQyOFa3CYwyJ-8DofWhgFJ6M8kCdkBjdO6p0YaeQ2YqfOVihSvAAFReJ0Lwbp+1+miNYcA6ZLiRmpImSbbpFC3C+Lib4-+ACUYrMCvstkWzgbcz2c9dkhy3DHPsG4c5ZBARXMeRQIah175Pz+Y5Oh2Y1owTAG0uqAASYAuzVmw3WZ2X0XY9jEtJYCiFbIqWbPlFmP5dyxFHWhjBF07k6qYqVLiygOhxi4GfOGZpeKCWdKmb-Hpvo9gHWAEgJx4ctzKpouHPY+gkpF2FZwYYsYyQgH9D2DlI5VEHy5YcMciBpQOWAIKsAerRUfXFS0-FvLQYlPBmrGmewcnlMqaveU8rMVKpVavNVEaKBaoMFAXV4wDU6SNU-Ec+ih7qlit9dCFrfUArWS9UK1kGa5p+jTcFM94bWXQnSgtVawAOPzeSwte8s3YzMfcsFZLPLnTdkU2tzb61yOjrsn8bIYAYGYsWUs7FOGBAgJKJgBxCwBAZssh6Y6J3gXGHO4YJTECroyUjaAkA+FLNHZDCdU7WJljnZWastZh6HqMuy9liY1S8ppjZYd5F-jGTjRGU6EYMg0WBQsZYbcvowHMPYc13qEpph8m1DAKGACCloGAZSEntDAY4mAmkRHsPQvSDIHWHm4HJ4ckNR3vm8S9EBu6qMaX6pZbV4GBUKGFEAdc9B7F4Ox5IAFXrWQCDxpJ99+OBA4xs0T3dBBsak4J9ksmty3FuWRzNgEQI2MiFgIQ0oszEr0L6LMtTo7kdOjWj9tr4F6H6E3CjLxV5gD2Fp+wZn5HQwoKGPQRZ2Z6R3qoiRtUaZ6LXSY5ybcGYmtzCOP9hNIOzh-KiY8zixRwfipapKOb4NAatkoYWRlS3U2hpusE16Z1xkoWJfM0p1BKCUIQQQzWbjehACW+DWWoAYDKxgAAXgASXYledC-AOuZbzRu+jCVqA7soY64EapYPtZEQVcihFTyYe0rpH9DpoDT0jLDM+-Q7UgFwR5niTAnj2FOy4IQdHx2XF8YgTs8BCg3i8BJF8K5fEhAAMpYhwl4fUuQ+t9ZgC0PTYRoEMyFJAGACBDvGgu5EeHB2jtHhOwEBzwAytbiYyhGMOl-Qo4qExxDuHmDSmYEwECWlYak7h-txHR3Sdo5Z8aTHp2HMBBe29j7X3tNvlEuJQo8AclTg7J2c74XsT5HF+QU7f2sC5B0JeXBbgMDeaxH5zMAWbTFPimAFXauYB6Vh5QZn8Akd1Qu2++0wWpFJUjL9YTi8c7zRo+RCz0iVG5Yp5vbmO8B3BTdx8j3CpxspUd6F2ReV7fkSCmyYUbwTeXhlLtyIeqZTkb0KNnseenBuDcWN-Yudc6Z4qJ9QmVkkNXW2SDYrRNuvdrhm7u+B1Pr9E0rX4arHIg1+rYnEJX01a9-zjE-p5FB-VzcDNUfP1x9z9Y5oNRUAVzbwN8AWBMpzohEzPwRUAAyI-u-q1OFsUoUnj9FSd88S03uuBa+Zm74D2vvAlrLA3hBcY0pP8rQf1RD0lOVf0WFrwwBgG8GWD2nCwYANRqHFAXXwwUGv0N2j0kRphd2TDd1NBgkf1rzvjZVWyQntB-AyFeACwMj+SY2syd2Sgz1NQyxSg2zPG23N2flEVEQFA2g4THAOEoJ9HI2xHQh1AVjYIu2xB62SAmD0g6C4ISD4MVzMwpC4CpBpCiGaBZHZHiEh2sBCCIHACt2c1oCEiUCnHUGNUIBMElksFJAMLc3gFoEsOsPMEsEaXsJmG01oC0OBRoxFwODk24iwiB0CI6EdWdS3HODN34AwGKCv3CAinCD0z6QSIMPZyo1oDECnDEGNW0BAF6AsHQHxUWAgAAHFJZi9mhyBjUgA);

The code is not yet documented. If you are interested, I will be glad to answer your questions...

```html
<html lang="de">
  <head>
  <meta charset="utf-8">
    <title>title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://efpage.de/DML/DML_homepage/lib/DML-min.js"></script>
  </head>
  <body> 

  <script> 
  function sleep (ms) {
    return new Promise((r,err) => {
      setTimeout(r,ms)
    })
  }
  
  class Board {
    
    cssCell = `
      background:blueviolet;height:100px; width:100px;
      margin:2px;cursor:pointer;flex: 0 0 100px;
      display:flex;align-items: center;
      justify-content: center;color: orange; 
    `
    
    cssBoard = `
      padding:5px;display:flex;
      flex-wrap:wrap;background:orange;
      width:416px;margin:0 auto;
    `
    

    checkValidBoard () { 
      function checkPos (c) {
        return c.dataset.pos == c.dataset.curpos
      }
      return this.cells.every(checkPos);
    }
    
    findDirectionToMove(cell) {
      
      const [getCol,getRow] = [(pos) => pos%4,(pos) => Math.trunc(pos/4)]
           
      const cellpos = Number(cell.dataset.curpos);
      const emptyCellpos  = Number(this.emptyCell.dataset.curpos); 
      
      const rowcolEmpty = [getRow(emptyCellpos), getCol(emptyCellpos)];
      const rowcolCell =  [getRow(cellpos), getCol(cellpos)];
      
      const sameColOrRow =  
      (rowcolEmpty[0] == rowcolCell[0] ) || (rowcolEmpty[1] == rowcolCell[1]);
      
      if (!sameColOrRow) return;
      
      const directions = {
        [ cellpos - 1] : "left",
        [ cellpos + 1] : "right",
        [ cellpos - 4] : "up",
        [ cellpos + 4] : "down" 
      }
      let posdir = Object.keys(directions).find(pos => pos == emptyCellpos);
      return (directions[posdir]);        
    } 
    
    findOpositeDirection(direction) { 
      if (direction == "up") { return "down" }
      if (direction == "down") { return "up" }
      if (direction == "left") { return "right" }
      if (direction = "right") { return "left" }
    }
    
    moveCell (cell) {
      const direction  = this.findDirectionToMove(cell);
      if (!direction) return;
      
      const amount = 104;
      const duration = 200;
      const coef = {
        'left' : -1,
        'right' : 1,
        'down' : 1,
        'up' : -1
      }
      
      const oppositeDirection =  this.findOpositeDirection(direction);
     
      const cssTranslate = (cell, direction) => {
        if (direction == 'left' || direction == 'right') { 
          cell.dataset.dx = Number(cell.dataset.dx) + amount * coef[direction]
        }
        if (direction == 'up' || direction == 'down') {  
          cell.dataset.dy = Number(cell.dataset.dy) + amount * coef[direction]     
        }  
        return `translate(${cell.dataset.dx}px, ${cell.dataset.dy}px )` 
      }
      
      cell.animate( 
        {transform: cssTranslate(cell, direction)}, 
        {duration: duration, fill: "forwards"});    

      this.emptyCell.animate(
       {transform:  cssTranslate(this.emptyCell, oppositeDirection)}, 
       {duration: duration, fill: "forwards"} 
      );

      let temp = this.emptyCell.dataset.curpos;
      this.emptyCell.dataset.curpos = cell.dataset.curpos;
      cell.dataset.curpos = temp;
      
      if (cell.dataset.pos !=  cell.dataset.curpos) {
        cell.style.background = "deeppink";
        cell.style.color = "white";
      }
      else { 
         cell.style.background = "blueviolet";
      }
    }
    
    createCells () {
      // fills cells array with cell element
      this.cells = [...Array(16)].map((_, i) => { 
        let options = {
          style : this.cssCell,
          ["data-pos"] : i, ["data-curpos"] : i,
          ["data-dx"] : 0, ["data-dy"] : 0
        }
        let content =  h1(`${i}`);
        let cell = create("div", options, content);
        cell.onclick =  () => this.moveCell(cell);
        return cell;
      });
     
      // empty cell customisation 
      this.emptyCell = this.cells[15];
      this.emptyCell.style.background = "rgba(255,0,0,0.0)";
      this.emptyCell.style.zIndex = 0;
      this.emptyCell.style.color = "transparent";
    }
    
    displayBoard () {
      selectBase(div(""));
        print("<h1 style='text-align:center'>Sliding Puzzle</h1>");
      unselectBase();
      selectBase(div("", {style: this.cssBoard}));
        this.cells.map(appendBase);
      unselectBase();
      selectBase(div("", "text-align:center;margin-top:20px"));
        button("Shuffle") .onclick = () => this.shuffle();
      unselectBase();
    }
    
    moveCellByCurpos(cellpos) {
      let cell = this.cells.find(cell => cell.dataset.curpos == cellpos);
      this.moveCell(cell);
    }
    
    async shuffle () {
      for (let i = 0; i < 200; i++) {
        const emptyPos = Number(this.emptyCell.dataset.curpos); 
        const validPos = [
            emptyPos - 1, emptyPos + 1,
            emptyPos - 4, emptyPos + 4
          ].filter(pos => {return (pos >= 0) && (pos <= 15);
        }) 
        const randomPos= validPos[Math.floor(Math.random()*validPos.length)];
        await sleep(90);
        this.moveCellByCurpos(randomPos);  
      }
    }
    
    start () {  
      this.createCells ();  
      this.displayBoard(); 
    }
  }
  
  function main() {
    let b = new Board();
    b.start()
  }
  
  main();
  </script>
  </body>
</html>
```