DEV Community

Cover image for Creating a Tic-Tac-Toe NodeJs Game
James Sinkala
James Sinkala

Posted on

Creating a Tic-Tac-Toe NodeJs Game

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.

GitHub logo xinnks / tictactoe-nodejs

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 {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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])}`;
Enter fullscreen mode Exit fullscreen mode

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
    })
  }
Enter fullscreen mode Exit fullscreen mode

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();
      }
    })
    ...
  }
Enter fullscreen mode Exit fullscreen mode

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()
      }
    })
...
Enter fullscreen mode Exit fullscreen mode

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();
        }
      }
    ...
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (3)

Collapse
andreasvirkus profile image
ajv

It'd be nifty to clear the terminal after each move, that way you'd create an illusion of updating the same board.

Also, I'd definitely bomb a live coding interview, whatever they'd ask me to do in 30 minutes đŸ˜¬
Thanks for sharing! :)

Collapse
xinnks profile image
James Sinkala Author

Thought so too, at the time I tried implementing that and it gave me some unpleasant result, since I was timing the task I pushed to working on that later, which hasn't arrived yet.

Collapse
codebyjustin profile image
Justin

Saving this to do later. Can't wait to wreck the terminal.