JavaScript tic tac toe - beginner level - t24

Sk - Sep 9 '22 - - Dev Community

Hi there👋🏽.

Introduction

As a beginner the best way to level up is to actually build complete programs, I use the word "complete" loosely. I am hoping to write a series where we will build 24 applications or programs together, varying in levels from beginner to advanced. for example : this tic tac toe mini series

for beginner level we will build just a working tac in the console,
for intermediate we will migrate tac to the canvas(mapping a data structure to canvas objects)
for advanced we will implement minMax algorithm

most of the projects in what I call a t24 series will follow this three part layer. These projects were actually suppose to be part of an ebook I was working on, to release under pay what you want, but I decided it is way easier to write articles and make it accessible for everyone, and to be fair I also want to improve my writing as I am not a native English speaker, I thought this was a perfect way, lot of writing etc.

let's get coding

here is my folder structure, we will update it as we go along, I do actually suggest that you put all the projects under the same folder prolly called t24.

folder structure

if you get stuck: repo

prerequisites

I am assuming you are familiar with JavaScript fundamentals: Objects, Arrays and Functions, if you want to level up a bit I also have a series for intermediate JS(which will be helpful as we move along the levels) you can check it out at your own leisure.

You need to be also familiar with tic tac toe or as we call it here "X's and O's"

tic tac toe

we will use a hardcoded 3 x 3 grid to represent our game space, we will prolly make this dynamic in other levels

file structure:



  tic\
     tic.js



Enter fullscreen mode Exit fullscreen mode

open tic.js and let's start coding

representing the board:



// array of arrays
const board = [[" ", " " , " "], 
               [" ", " " , " "], 
               [" ", " " , " "], 

]



Enter fullscreen mode Exit fullscreen mode

the idea here is simple since we are in the console we will console.log our board after every update.
update being whether the player or computer puts an X or O on a free space in our board, it will all make sense as we code.

we will use an object to encapsulate all of our gaming logic



const game =  {
      update : function(){},
      isgameOver: function(){},
      move : function(c){},
      possibleMoves: function(){},
      computer: function(){},
      updateBoard: function(){}

}  



Enter fullscreen mode Exit fullscreen mode

I suggest typing along as we fill these functions rather than copying as you'll learn more.

updateBoard

prolly the simplest function, all we need to do is loop over our array of arrays and log them as a tic tac toe board
| |
| |
| |



   updateBoard: function(){
        console.log("   ") // empty space before a board
        board.forEach((arr, i)=> { // looping over each array in the board
            console.log(arr.toString().replace(/,/g, "|"))
        })
    }




Enter fullscreen mode Exit fullscreen mode

the magic in update board happens here:



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


Enter fullscreen mode Exit fullscreen mode

if you understand this you good to go, basically what we are doing here is turning an array [" ", " " , " "] to a string and replacing every , with a | symbol, basically if we had an array ["O", "X" , "O"] the above code will turn it to string O|X|O, which is what we are doing to log our board, you should end with a 3 x 3 board like this:

Image description

this function is actually complete, next let's implement possibleMoves, the second easiest func to implement, we will worry about composing everything together later

possibleMoves



 possibleMoves: function(){
        const p = []
         // just looping over the board O(n*2)
        for(let i = 0; i < board.length; i++){
            for(let j = 0; j < board[0].length; j++){
               if(board[i][j] === " "){ // if we find a space without X or O add it to p array(it's a possible move)
                   p.push({row: i, col: j})
               }
            }
        }

        return p
    }



Enter fullscreen mode Exit fullscreen mode

Our little loop takes O(n*2) time, which is not bad considering our board size, with wich we got coordinates for free spaces in an array.

Coordinates in this case are indexes to our array of arrays e.g
board[p.row][p.col] is how access a space.

let's bring some of this together, by implementing a part of the update function

update



let m = [] // place this outside our object, under board, 
           // m will hold all current possible moves



update : function(){
   this.updateBoard();
   m = this.possibleMoves();
}



Enter fullscreen mode Exit fullscreen mode

for now the update function is just drawing the board and getting all possible moves

let's implement a simple computer function, that will pick a random move, from possible moves and place an O, we will handle turns etc later

computer




   let turn = true // declare turn outside we will use it to change turns later

   computer: function(){

          console.log(m) // see all possible moves(for debugging purposes)
          if(m.length > 0){
              let ra = Math.round(Math.random() * (m.length - 1)) // picking a random number in a range of our board len
              board[m[ra].row][m[ra].col] = "O" // using that index to pick a random coordinate
          }
          turn = true;
          this.update() // important
          console.log("ur turn")
    },



Enter fullscreen mode Exit fullscreen mode

believe it or not, that's all for our computer, now let's implement the player which is responsible for starting the game via the move function, hang in there we are about to bring everything together, it will all make sense.

move

now this is the complicated part especially if you are not familiar with node js, I will try to explain everything

basically we need a way to process player input and get coordinates to make a move on the board(put an X)



// declare var on top 
let combo = {
    row: undefined,
    col: undefined
}




Enter fullscreen mode Exit fullscreen mode

combo will hold user input, the idea is simple when both row and col are other values than undefined e.g



 combo = {
   row: 0,
   col: 0
}



Enter fullscreen mode Exit fullscreen mode

move will put an x on 0,0. and hand over the turn to the computer function vice versa

getting user input



const readline = require("readline")  // way to import a module into node(comes with node)


Enter fullscreen mode Exit fullscreen mode

you do not need to know how the readline module works, just the part we interested in

at the bottom of everything write the following, which is responsible for waiting for user input in the console(think of it as a while true loop) but waiting for user input



readline.emitKeypressEvents(process.stdin);  
process.stdin.setRawMode(true);

// the following code is all you need to understand for now
// all it is saying is when we receive a key press event react
process.stdin.on('keypress', (str, key) => {
   // we only care about the key
  if (key.ctrl && key.name === 'c') {
     // if we receive ctrl-c we exit the "loop"
    process.exit();  // exits the program
  } else {
     // else if we get other keys and it's the players turn
      if(turn){ 
        if(combo.row){
          // if we got the coord for row fill col
            combo.col = key.name
            turn = false
            // basically we have all coordinates to make a move
            game.move(combo)  
         }else{
           // the first key we want is the row
           combo.row = key.name
         }
      }else{
          // pressing a key while it's computer's turn
          console.log("wait your turn")
      }


  }
});





Enter fullscreen mode Exit fullscreen mode

now we can implement the move function, as we can now get user input

move

remember move will only be invoked when combo has both row and col coordinates



  move : function(c){


        board[+c.row][+c.col] = "x"
        // turning combo back to undefined
        // so we can take coordinates for the next turn for player
        // if we don't do this we will never get new coords 
        combo.row = undefined
        combo.col = undefined
        this.update()
        setTimeout(() => {
             // after making our move we give CPU(I am tired of writing computer😂, it's cpu from now on) 
             // the turn 
            this.computer()
        }, 3000);
    },



Enter fullscreen mode Exit fullscreen mode

if you run the program now it should hang and wait for coordinate(input numbers), then it will execute move, which will call update and CPU, vice versa

we almost done, all we need now is gameover logic

gameover



  // write this function on top of everything
const allequal = arr => arr.every(v => v !== " " &&  v === arr[0]);



Enter fullscreen mode Exit fullscreen mode

if you understand the above function, you are basically done with gameover.

what this func is basically doing is checking for same contiguous values

e.g [1, 1, 1] it will return true, for [1, 2, 1] false,
meaning we can just put ["X", "X", "X"], and we will know X has won vice versa, all we need now is dumb logic to check all possible combos basically:

Image description

that is basically what the code below is doing
first on top declare winner and gameOver vars



   let winner = ""
   let gameOver = false;


Enter fullscreen mode Exit fullscreen mode

dumb logic



 isgameOver: function(){

    if(allequal(board[0])){
                      gameOver = true;
                  winner = board[0][0]

            }
              if(allequal(board[1])){
                      gameOver = true;
            winner = board[1][0]

              }
                if(allequal(board[2])){
                          gameOver = true;
              winner = board[2][0]

            }

             if(allequal([board[0][0], board[1][0], board[2][0]])){
                        gameOver = true;
                 winner = board[0][0]
             }
              if(allequal([board[0][1], board[1][1], board[2][1]])){
                        gameOver = true;
            winner = board[0][1]
              }
               if(allequal([board[0][2], board[1][2], board[2][2]])){
                        gameOver = true;
             winner = board[0][2]
               }
                 if(allequal([board[0][0], board[1][1], board[2][2]])){
                        gameOver = true;
               winner = board[0][0]
         }
                   if(allequal([board[0][2], board[1][1], board[2][0]])){
                        gameOver = true;
                 winner = board[0][2]
           }

  }



Enter fullscreen mode Exit fullscreen mode

all we need to do now is bring everything together in update

update



  update : function(){
    this.isgameOver();  // checking if game is over

    if(gameOver){
      this.updateBoard()
      console.log(`Game over ${winner} won!`)
      process.exit();  // exit ("loop")
      }
     this.updateBoard();
    m = this.possibleMoves();
    // if there no possible moves anymore it's a draw
    if(m.length === 0){
      gameOver = true;
      console.log("Game over by draw")
      process.exit();
    }

  }




Enter fullscreen mode Exit fullscreen mode

with that we are done with the beginner level, next article we are making it graphical and do a bit of clean up and error handling, you can do it as a challenge if you want

you can find the repo here for this code so far, watch, fork, star the repo all that good stuff I will be updating it as we go along, you can get the code even before the article for intermediate as I will commit as I code too

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!!

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