BlogHome
Creating a Tic-Tac-Toe NodeJs Game

Creating a Tic-Tac-Toe NodeJs Game

Feb 13, 2021Updated on Feb 6, 2022

Though early 2021 has been a year of new and interesting experiences for me. Around the new years eve I received an email from the co-founder of a certain company that I had applied for a part-time remote Javascript position some time back in 2019.

Long story short, I did an interview, and I think I might have flunked that one (Well, I'm getting the silence treatment). This was my first interview ever as I have always freelanced on my on. I was looking for the opportunity to work with a team even on a part-time basis to garner some new experiences on that environment and hopefully learn something new that I could apply in my work, also the extra income would be a welcomed plus.

As initially said, I think I flunked the interview and mainly on the coding task that was handed to me. I was tasked with creating a back-end Tic-Tac-Toe game (one to run on the terminal) in 30 minutes. Among the features to add to the game apart from figuring out when a player wins were knowing when it's a draw, assigning a certain key that when clicked undoes the previous move and some few other features that I can't recall.

This was my first instance coming across the Tic-Tac-Toe game and also another fault of my own was not exploring the back-end (terminal) NodeJs tools. I struggled with getting the inputs from the terminal as I had not worked with terminal inputs since I last worked with C++ and recently with RUST. I wasted a bit of time getting to grips with the game and the platform I was to use to write the code (repl.it) as they were both new to me. I didn't finish the task in time, but afterwards I took the time to do it on my own, researched a bit about getting input streams from the terminal with NodeJs and came across the Readline module that handles that and read a bit on NodeJs' process events.

I appreciated the experience but the only negative I can draw from it was from the company I was interviewed by, not that they are obligated to but a status update regardless the result would have been appreciated on my side considering they promised to update me three days after the interview, and the email I sent afterwards asking for just that.

With that out of the way let's proceed with what this blog is about. I decided to share the code to the Tic-Tac-Toe game that I worked on post interview with the rest of you. You can use this as a template and better it for fun or at the very least learn from it if this is all new to you. I definitely think it can be improved and will do so when I get the time. I have added processing the input stream and perfecting figuring out a draw as good first issues for anyone who will be interested to work on that on it's github repo.

A terminal TicTacToe game made for the terminal

Creating the game

I decided that the game should be within a class setup considering all the advantages that come with classes as opposed to throwing independent functions all over the place as they are quite a number of them.

const readline = require('readline');

'use strict';

class TicTacToe {
    ...
}

At the end of this tutorial the game should work as follows: Tic-Tac-Toe Game Demonstration

Plot the game's board:

this.ticTacToeLayout = `${this.displayItem(this.ticTacToe[0])} | ${this.displayItem(this.ticTacToe[1])} | ${this.displayItem(this.ticTacToe[2])}
---------
${this.displayItem(this.ticTacToe[3])} | ${this.displayItem(this.ticTacToe[4])} | ${this.displayItem(this.ticTacToe[5])}
---------
${this.displayItem(this.ticTacToe[6])} | ${this.displayItem(this.ticTacToe[7])} | ${this.displayItem(this.ticTacToe[8])}`;

which will give us the following on the board: Tic-Tac-Toe Board

To make this blog short to read as it's full source code is available on the github repo, I'll focus on the essential parts of this game.

Taking input streams:

Within the constructor of the class initiate the interface of the readline module that reads data from a readable stream in this case process.stdin.

 constructor(){
   this.rl = readline.createInterface({
     input: process.stdin,
     output: process.stdout
   })
 }

The best way to go about collecting input provided in the terminal in the game's scenario is to listen to the end of the input stream. We can use the readline listener to listen to the 'line' event that is emitted when the input stream receives an end of line input such as \n, \r, or \r\n which occurs when one presses enter or return.

  startGame(){
    this.displayLayout();

    // listen to inputs
    this.rl.on("line", (input) => {

      if(this.ticTacToe.length <= 9){
        // read move
        this.readMove(parseInt(input))
        // continue playing
      } else {
        console.log("Game Ended!");
        this.processGame();
      }
    })
    ...
  }

The second input we collect from this game is listening to the special button that when clicked undoes the previous move. We handle this at the end of the startGame() method above.

...
    // listen to delete events by backspace key
    process.stdin.on('keypress', (str, key) => {
      // delete move
      if(key.sequence === '\b'){
        this.deleteLastMove()
      }
    })
...

Every move made in the game is recorded by adding it into an array of moves made called moveRegister, what the deleteLastMove() method does is delete the last move from the moveRegister and undoes the last item added to the ticTacToe array which plots the X and O characters on our game board.

Processing the game

The other essential part of the game is processing the game on user input. Since the game board consists of nine possible positions where user data can be plotted and within Tic-Tac-Toe the first user that is able to create a straight line of three of their characters (X or O) wins the game we search just for that in the game, looking for all the possible occurrences of straight lines made by the same user between the two players. The method processGame() does just that.

  ...
  processGame(){
    // at least 5 moves need to have been made
    if(this.moveRegister.length >= 5){
      var checkSet = new Set()
      // possible vertical alignments
      if(this.ticTacToe[0] && this.ticTacToe[3] && this.ticTacToe[6] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[3]).add(this.ticTacToe[6])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[0])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      if(this.ticTacToe[1] && this.ticTacToe[4] && this.ticTacToe[7] && (Array.from(checkSet.add(this.ticTacToe[1]).add(this.ticTacToe[4]).add(this.ticTacToe[7])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[1])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      if(this.ticTacToe[2] && this.ticTacToe[5] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[2]).add(this.ticTacToe[5]).add(this.ticTacToe[8])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[2])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      // possible horizontal alignments
      if(this.ticTacToe[0] && this.ticTacToe[1] && this.ticTacToe[2] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[1]).add(this.ticTacToe[2])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[0])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      if(this.ticTacToe[3] && this.ticTacToe[4] && this.ticTacToe[5] && (Array.from(checkSet.add(this.ticTacToe[3]).add(this.ticTacToe[4]).add(this.ticTacToe[5])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[3])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      if(this.ticTacToe[6] && this.ticTacToe[7] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[6]).add(this.ticTacToe[7]).add(this.ticTacToe[8])).length === 1)){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[6])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
      // possible diagonal alignments
      if((this.ticTacToe[0] && this.ticTacToe[4] && this.ticTacToe[8] && (Array.from(checkSet.add(this.ticTacToe[0]).add(this.ticTacToe[4]).add(this.ticTacToe[8])).length === 1)) || (this.ticTacToe[2] && this.ticTacToe[4] && this.ticTacToe[6] && (Array.from(checkSet.add(this.ticTacToe[2]).add(this.ticTacToe[4]).add(this.ticTacToe[6])).length === 1))){
        console.log(`Player ${this.getPlayerFromChar(this.ticTacToe[4])} Wins!!`);
        this.endGame();
      }
      checkSet.clear();
    }
  }
  ...

Hopefully this game's source code helps some of you in your future interviews or in your adventures with the terminal side of NodeJs.

Go ahead and wreck the terminal.