JavaScript tic tac toe - intermediate level - t24

Sk - Sep 11 '22 - - Dev Community

Hi there👋🏽.

Intro

Translating a data structure(DS) to a visual component is an essential skill for a developer, especially as a JS dev, as you may find yourself dabbling in the frontend from time to time, which is what we going to do in this tutorial.

this is a build from the previous tut JavaScript tic tac toe - beginner level - t24 where we built a working console logged tic tac toe, you can find the code on git.

Image description

Intermediate

Translating a DS into a visual component is part of the intermediate, However the true intermediate comes from handling two environments, as now we are wrapping a node/native app for the browser, which is very exciting actually.

Node vs Browser

Node and the browser are completely two separate distinct environments that JS can run in, Node js(the site) words it way better "Unlike the browser where JavaScript is sandboxed for your safety, node. js has full access to the system like any other native application"

let's get crackin

before we do anything we need a way to detect or tell whether our code is running in the browser or node, for example the browser does not have the readline module while node has no document(DOM), so we need a way to run specific code for a specific env;


  let inNode = true;  // on top

if (typeof process !== 'undefined') {
  // any code specific to node should be in this block and will not run in the browser
  // as process is not defined in the browser but node only
  // since readline is in node we need to move it's require here



}else{
   // browser specific code is controlled by this else 
    // inNode being false
  inNode = false;

}




Enter fullscreen mode Exit fullscreen mode

moving node specific code

// move the readline emitter and require here
// first declare readline on top, as it will hold our readline module on load
let readline;   // will remain undefined in the browser
let inNode = true;

if (typeof process !== 'undefined') {
 readline = require("readline");  
 readline.emitKeypressEvents(process.stdin);
 process.stdin.setRawMode(true);
 process.stdin.on('keypress', (str, key) => {
   if (key.ctrl && key.name === 'c') {
     process.exit();
   } else {
       if(turn){
         if(combo.row){
             combo.col = key.name
             turn = false
             game.move(combo)
          }else{
            combo.row = key.name
          }
       }else{
           console.log("wait your turn")
       }


   }
 });

}else{
   inNode = false;

}



Enter fullscreen mode Exit fullscreen mode

as simple as that now we can run browser and node specific code in one file.

Browser side

here is a simple html file:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *{
            box-sizing: border-box;

        }

        body{
            padding: 0;
            margin: 0;
            font-family: sans-serif;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;

        }

        .app{

            display: flex;
            flex-wrap: wrap;

        }
        .reset{
            width: 0px;
            height: 0px;
            border: 1px solid black;

        }

        .img{
          /* width: 100px;
          height: 100px; */
          object-fit: cover;

        }
    </style>
</head>
<body>

      <div class="app">


      </div>



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



Enter fullscreen mode Exit fullscreen mode

I won't be explaining the css, as I assume it's very easy as this also is an intermediate tut

Out goal here is translate our board :

const board = [[" ", " " , " "], 
               [" ", " " , " "], 
               [" ", " " , " "], 

]





Enter fullscreen mode Exit fullscreen mode

to a visual board in the browser, so I had two options: to use elements or the canvas, I picked elements, as the canvas is kinda complex but way faster, speed won't be a problem for us thou since our board is small. Maybe in the advanced level we will have a side by side, elements and canvas.

first things first we need to update the updateBoard function to not console.log in the browser

updateBoard: function(){

     if(inNode){
        console.log("   ")
        board.forEach((arr, i)=> {


            console.log(arr.toString().replace(/,/g, "|"))


        })
    }else{

    }
  }


Enter fullscreen mode Exit fullscreen mode

the following is browser code block, everything we do from about now, will be in this block

if (!inNode) {

   // will run in the browser only


}



Enter fullscreen mode Exit fullscreen mode

I suggest using VScode live server plugin to run this, using tools like Parcel will cause problems, as they transpile the code, I assume, you can try thou.

let w = 0; // declare on top 


if (!inNode) {

    let app = document.querySelector(".app") 

    w = Math.floor(300/board.length - 2);
     // w is the width of each cell
    // our app div is 300 x 300 px, we are dividing by
    // board len(rows) so each cell will take up equal space
    // basically creating a grid with equal spaces
    // the minus 2 accounts for border line in each cell
/**
 * @type {HTMLDivElement}
 */

  const len = board.length 

  for(let i = 0; i < len; i++){
    for(let j = 0; j < len; j++){
       // create cell will return a div
      app.appendChild(createCell(w, i, j))
       // we are passing i and j so each cell is aware of it's position(coordinates)
       // which correspond to coordinates on the board
    }


 }


}


Enter fullscreen mode Exit fullscreen mode

the createcell function

let cells = {} // declare on top,
...

if (!inNode) {



     function createCell(w, col, row){


           // closure function, 
           // to make sure we receive the correct state onclick
          function keepContext(){

            let state = {
              col, 
              row, 
              w
            }

            let d = document.createElement("div");
            d.style = `background: lightgray;`
            d.classList.add("reset")
            // stretching the empty div width and height
            d.style.paddingBottom = `${w}px`
            d.style.paddingRight = `${w}px`
            d.onclick = () => processClick(state)
            // cache of the created cells(elements), so we 
            // use the elements in the updateBoard func to
            cells[`${row}-${col}`] = d
            return d

        }

      return keepContext()
     }


 ...

}


Enter fullscreen mode Exit fullscreen mode

all we need now is the processClick function

if (!inNode) {

  function processClick(state){
         const img = document.createElement("img")
         img.src = "./img/X.png"  // an Image with x, you can use any you like, I drew it with excalidraw, make the background transparent
         img.height = state.w
         img.width = state.w

        // magic happens here: 
        if(board[state.row][state.col] === " " && turn){
         // if the user clicks on empty cell and it's not computer playing
         // append an image on the click row and col
         cells[`${state.row}-${state.col}`].appendChild(img)
         // also update the board(remember the coords correspond)
         // so the computer knows we played and can see updated possible moves
         board[state.row][state.col] = "X"
         // update possible moves
         game.update();
         setTimeout(() => {
          game.computer()
      }, 300);
        }
     }


...
}

Enter fullscreen mode Exit fullscreen mode

all we need to do now, is update the visual board from the state of the array board, so they are in sync,

in the else of updateBoard

else{
      // console.log("updating viz board")
      board.forEach((arr, i)=> {
           arr.forEach((val, j)=> {

             if(val === "O"){
                  // checking if there's no O image in the current cel
                if(!cells[`${i}-${j}`].firstChild){

                  const img = document.createElement("img")
                  img.src = "./img/O.png"
                  img.height = w
                  img.width = w
                  cells[`${i}-${j}`].appendChild(img)
                  // adding O's in the visual board
                }
             }
           })
      })


    }


Enter fullscreen mode Exit fullscreen mode

remember X's are added onclick,

The only thing left now, is to sync the visual board with the states of winning and draw, meaning the player and computer cannot add O or X when the game is over,

it's a bit messy, but it works, in the advanced level, I will consider refactoring

 ...
 update : function(){
    this.isgameOver();

    if(gameOver){
      this.updateBoard()
      console.log(`Game over ${winner} won!`)
        if(inNode){
          process.exit();
        }else{
          turn = false
        }
      }
     this.updateBoard();
    m = this.possibleMoves();
    if(m.length === 0){
      gameOver = true;
      console.log("Game over by draw")
      if(inNode){
        process.exit();
      }else{
        turn = false
      }
    }

  }

...

Enter fullscreen mode Exit fullscreen mode

With that we are done with the intermediate level, as the levels increase the explanation does the opposite, if you get stuck use the repo**** as a reference

Conclusion

I will try to make this series as consistent as possible, More interesting projects coming, including a mini compiler👌🧙‍♂️, yes I am going all out, teaching is the best way to learn too,.

if you would like to support me, you can buy me a coffee below, I will highly appreciate it❤❤, you certainly don't have to if you can't.

Buy Me A Coffee

with that cheers till next time, Happy Hacking!!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .