Tutorial — Tic-Tac-Toe Game with Vanilla JavaScript

This is a tutorial on how to build a very basic tic-tac-toe game with vanilla javascript, html, css.

Duke Nguyen
The Startup
10 min readJun 18, 2020

--

Final result
Final product

Github Repo: https://github.com/dukeraphaelng/tic-tac-toe

Codepen: https://codepen.io/dukeraphaelng/pen/wvMgpmP

Features

  • Board size is dynamically generated upon user’s input instead of being static and hard-coded.
  • Winning score can be set manually.

Update (19 June 2020)

  • This app now has a score counting, reset, and changing player’s avatar feature. The guide to this second part is coming soon.
Codepen

HTML

Step 1: Create index.html. Generate a boiler plate through snippet code and attach css and js external files

Step 2: There are many ways to represent a board for a game like tic-tac-toe. However, in the current implementation, I have chosen to use HTML table. In addition, in any kind of boardgame, we must record the coordinate of the pieces in order to store user moves in the database (e.g. to check whether the winning condition has been met). In this implementation, I have chosen to record the coordinate using the following pattern: each row would have a class of the x coordinate (e.g. “x1”), and the row would contain the number of cells being equal to the number of columns, having the following id format “x[x-coordinate] y[y-coodinate]” (e.g. “x1 y2”). I would later parse this into a JavaScript object to manipulate in the database.

The previous code is to be added in the <body>

Step 3: Additional features that will be built later: warning & winner announcement

You can decide how to organize your UI elements, but I prefer them at the top right above the tic-tac-toe table.

CSS

Step 4: Create style.css, set table sizes, and display the body with flex, justify-content, align-items, text-align center to have everything right in the middle. Add borders to all <td> to have an outlined table.

JavaScript

Step 5: Code planning
Coding becomes much more efficient if the programmer first imagines the structure of the program. This is especially needed since the implementation of my features produces 230 lines of code in JavaScript. To imagine the structure of this program, we should write some pseudocode.

Step(s):

  • 1–5: Create a basic structure, with a table with each cell as a box where either X or O can be filled. (HTML & CSS: DONE)
  • 6: Store all DOM elements as attributes inside an object for DRY. We can then access all DOM elements by calling properties from this object.
  • 7: Create a STATE object which represents the current state of our application (the current player, moves each player has made, the board size, the winning requirement, and player’s name). When we want to modify any attribute of the current state (e.g. change to the next player, add an additional move to the current player, changing to winning point or the board size through inputs), we can simply modify the property of this object.
  • 8. Create a MAIN function which when called will start the game. The game is started when the moment the player clicks on a tile, the tile is filled.
  • 9. Create a INSERTTOKEN function called when a tile is clicked. This function will first check if the current tile is empty, and if it is not then a warning is displayed, otherwise the function runs. First the UI of the tile is filled with the current player’s token and secondly the filled tile’s ID is parsed from HTML id into a JavaScript object which is subsequently saved to the current player’s move list. Then the game is switched to the next player.
  • 10. At this point, the game is functional, but no winner is notified when the winning condition is reached. Building this functionality is perhaps the most difficult task in the application, mainly because I do not wish to hard code winning scenarios but allow for dynamic board generation (e.g. the algorithm would work at any board size and with any winning score), for the later addition of these settings. Certainly there are many ways to approach this task, but I believe my approach is one of the less memory intensive ways. The idea is that each token when place holds has its own score tracker. This tracker would keep track of the token’s size along the x, the y, and the two diagonal axes. Whenever a token is newly placed, it will have score of 1 along the 4 axes (four 1 values). If another token of the same player is placed next to it along any of the axes, its score is added by 1 (the score of the newly added one), whilst the newly added score is added by the score of the previous token. Since the previous token could be sitting on that axis already with 2 more tokens in a row (hence totaling 3 points for that axes). Then each of those tokens on the axes would each respectively have a score of 3. Every time a token is placed, the winning condition is assessed. By default, the required score is 3. Hence the moment a token reaches 3 points, the player who just places it has won.
  • 11. Once this is built, changing the size of the board and the winning requirement is as easy as a piece of cake. On the HTML template, once the player puts in the appropriate numbers and press submit, these numbers will be validated (whether they are a number, or whether they are empty), if they are valid, then the <tbody> is emptied, and a loop constructed with the number of iterations equalling to the board size input, with each iteration create a row <tr> with the class according to the iteration, and then a loop is nested again to create each cell in <tr> with the id coded in. Then all event listeners are removed and re-applied on the newly created tiles. And that’s it!

Step 6: Store all DOM elements inside an object

In the current implementation, I have chosen to represent x and o with the tile background colour of blue and green (I will later apply style change), but you can choose to insert images here as well, some of the code will be slightly modified in the UI display. One piece of code in this section also stands out: getting the array for all tiles. Queryselectorall is one of the main methods to return a collection of elements in JavaScript, however, it does not return an array but a nodelist, we have to convert it into an array using Array.from(). I also put this in a separate function as later on when we add change board size functionality. It needs to be recalled as the default 3x3 tiles will be removed from the UI and replaced by HTML code in JavaScript which needs to be restored.

Step 7: Storing the current state of the application inside of a STATE object

The pieces that stand out in this section is the player: { x: [], o:[]} nested object. This stores all the moves (all the tiles that have been placed) of each player.

Step 8: Create a game-starting MAIN function

The ‘click’ event is listened on for each tile in the DOM.tiles that we just parsed from the nodelist earlier, which is handled by the main driver of the application: insertToken().

Step 9–1 Constructuring the insertToken function

The insertToken is activated every time the player presses a new tile. The first action is to clear the previous ‘Tile already placed warning’ if it was activated. Then the exact tile is parsed from event.target, and the if statement is run to check whether the tile is empty with a tileEmpty function which we will craft later. If this is false, then we will display a warning with tileNotEmptyWarning() function. These will be detailed below. If the tile is indeed empty, we first modify the UI. In my case I will change the background colour of the tile, but I have also included code that you could use to insert and remove photos if that’s what you want instead. Afterwards the id of the tile is parsed into JavaScript object format with the tileJSPosition to get the coordinate/position of the tile. This tile is added to the player’s move array with the addTokenToState method. Afterwards, the tile’s point tracker is processed, this method also checks the winning condition after calculating the tracker point. If the winner is yet to be determined the switchPlayer function is called.

Step 9–2: Check whether tile is empty and display warning or clear warning functions

These are a group of three ‘tile not empty’ functions. The first — tileEmpty(tile) checks whether the backgroundColor is empty, (or in the case of inserting photos, if the innerHTML is empty). The second — tileNotEmptyWarning() displays a warning in the alert div if the previous function returns true. The third function — clearWarning() is run every time a new click on a tile is received, clearing the inner text of the alert div.

Step 9–3: HTML Tile ID is converted into JavaScript Object tile position

After changing the UI when clicking on an empty tile, tileJSPosition(tile) gets tile.id when called then .split(“ “) with a space to get to seperate “x[num]”, of which position is splitTile[0], and “y[num]” coordinate, of which position is splitTile[1]. Then both these are .split(‘’) character by character slicing off the first character (x/y) and then rejoining and parseInt() to get the final x, y coordinates in JavaScript Object. The object looks something like this when the middle tile is clicked in a 3x3 {x: 2, y: 2}.

Step 9–4: The tile is added to the state

Since the tile is placed by the current player before switchPlayer(), the array of the player’s move can be accessed with state.player[state.currentPlayer]. the whole previous object is assigned to tileObj, and then saved as the tile position. Additional info include a base point of 1 for each axis (x,y and the 2 diagonal axes).

Step 9–5: Switching player after move is done and all processes for the previous player finished

This is a simple ternary function, which toggles state.currentPlayer. Now that we have visited, explained and built all the supplementary functions of insertToken, we need to get to the most important piece, the function that gets executed before switchPlayer — addPointsToToken(tileObj)

Step 10: Adding points to tile’s point tracker and checking winning condition

addPointsToToken(tileObj) is composed of three functions called on four axes. winThroughXorY is called on both ‘x’ and ‘y’ axes, then the two diagonal axes are called with winThroughDiagonalTopLeft and winThroughDiagonalTopRight.

The following is the code for winThroughXorY

state.player[state.currentPlayer].forEach((aTile)=> {}) iterates through each/ every/ a tile in the player’s move array. Then an if statement is checked whether

returns true. This condition checks whether along the x or the y axis if the tile that was just placed is connnected with previously placed tiles of the same player. If this condition is fulfilled then mutatePoints() will add the total point of all tiles along x/y to each tile.

To demonstrate this we have the following examples of two cases of X winning with 3 consecutive tiles. The coordinates for the first case are: X2 Y2, X3 Y2, X4 Y2. The coordinates for the second are X5 Y3, X5 Y4, X5 Y5.

In both cases, winning coordinate’s number stay the same, whilst the other coordinate numbers ascend (e.g. in the first case, Y2 stays the same, and X ascends with X2–3–4; in the second case, X5 stays the same, and Y ascends with Y3–4–5). Nevertheless, depending on how we count it, it can be either descending or ascending. As a tile can both be placed on the ascending or the descending side of the sequence.

Thus we can check coordinate ‘x’ with

The first phrase before && checks whether the Y coordinate stays the same, and the second phrase checks whether X coordinate either ascends or descends. If we generalize this for both x and y cases then we get the above algorithm. Then we only need to call the algorithm to both X and Y axes. One of the supplementary function was reverseCoordinate, which is a simple toggle string function between ‘x’ and ‘y’.

If this condition is satisfied then mutatePoints(aTile, tileObj, coordinate) is called.

mutatePoints add the point of the selected coordinate case from the current tile (tileOblj) to the tile that satisfies the condition (aTile), and re-assign that value to the tileObj’s point of that coordinate.

Afterwards, the winning condition is checked, and if it is true then the winner is declared

If the point of the tileObj at the selected coordinate is greater than or equal to the state.toWin variable then the winner is set.

The winner is called by simply modifying HTML elements in the UI

Once the winner has been declared. The game must be stopped from further clicking, this can be done by removing all event listeners.

That is the end of winThroughXorY

The following is the code for winThroughDiagonalTopLeft and winThroughDiagonalTopRight

With the same logic, we first examine the case examples in the following excel examples

The winning sequences here are: (X2 Y3, X3 Y4, X4 Y5) & (X4 Y7 X3 Y8 X2 Y9). The pattern in these sequences is that for the first case with the top left being on the higher end, both X (X — 2–3–4) and Y (Y3–4–5) coordinates ascend proportionately, whilst for the second case, X and Y coordinates have an inverse pattern: while X descends (X4–3–2), Y ascends (Y7–8–9). By applying the same logic as the previous code we will be able to produce the code above. With this we have reached the end of step 10.

Step 11: Additional setting functionalities

To be able to dynamically generate empty tiles depending on board size input, all event listeners must be removed from default tiles (as they will eventually be removed). Hence gamePlayOff() must first be called. Then the board size number is validated (against being not a number and being empty), if this condition is satisfied, then the board’s html is removed and for the size of the board size input, each row and each cell in rows are created accordingly as shown above. Subsequently, the winning requirement number is validated before it is assigned to the state variable. After all these steps, the game is started by calling main(). However, finally and most importantly, these need to be called when the submit button is pressed for the inputs, hence we need to add an event listener on this element

This is the end of this tutorial. I hope it has not been ‘too long’.

Happy coding :)

--

--