Skip to content

HTML games programming from the ground up: Starting the Tic Tac Toe game

You can see an index of all the posts in this series: go to index.

I want to create a simple, responsive version of Tic Tac Toe that looks attractive and offers the possibility for games with any combination of human and AI players. The AI players should offer a range of difficulties from not at all challenging to difficult to beat.

Although I’ve said I want the game to look attractive, before I turn my attention to creating a slick user interface, I need to focus on building the core game. For that reason I’ll start with a very rough and ready user interface that is good enough for testing the game functionality.

Here’s what the initial HTML looks like:

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Tic Tac Toe</title>
    <link href="css/tictactoe.css" rel="stylesheet">
    <script src="js/tictactoe.js"></script>
  </head>
  <body>
    <div class="game_layer">
      <!-- board -->
      <div class="board">
        <div class="cell_container"><div class="cell_id">1</div><div class="cell_content" id="cell0"></div></div>
        <div class="cell_container centre"><div class="cell_id">2</div><div class="cell_content" id="cell1"></div></div>
        <div class="cell_container"><div class="cell_id">3</div><div class="cell_content" id="cell2"></div></div>
        <div class="cell_container middle"><div class="cell_id">4</div><div class="cell_content" id="cell3"></div></div>
        <div class="cell_container middle centre"><div class="cell_id">5</div><div class="cell_content" id="cell4"></div></div>
        <div class="cell_container middle"><div class="cell_id">6</div><div class="cell_content" id="cell5"></div></div>
        <div class="cell_container"><div class="cell_id">7</div><div class="cell_content" id="cell6"></div></div>
        <div class="cell_container centre"><div class="cell_id">8</div><div class="cell_content" id="cell7"></div></div>
        <div class="cell_container"><div class="cell_id">9</div><div class="cell_content" id="cell8"></div></div>
      </div>

      <!-- game status -->
      <div id="inPlayControls">
        <p>It's <span id="turn"></span>'s turn to play.</p>

        <!-- user input -->
        <input id="selectedCell" type="number">
        <button id="play">Play</button>
        <button id="quit">Resign</button>
        <p id="valWarning" class="warning hidden">Please enter a cell number between 1 and 9!</p>
        <p id="posWarning" class="warning hidden">Please enter a number for a cell that is empty!</p>
      </div>

      <!-- end of game info and controls -->
      <div id="outOfPlayControls" class="hidden">
        <p id="winMsg"><span id="winner"></span> wins!</p>
        <p id="drawMsg">The game is drawn!</p>
        <button id="new">New Game</button>
      </div>
    </div>
    <!-- script to boot game once DOM loaded -->
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        tictactoe.initialiseGame();
      });
    </script>
  </body>
</html>

You can see that the page is divided into two main sections. The first of these is the board. This is a set of nine containers, one for each cell on the board. Each of these contains two divs. One will hold the number identifying the cell – players need to see this in this version so they can indicate which cell they want to play into. The second will hold the mark, showing the piece that has been played, either ‘O’ or ‘X’.

The next section shows the game status and the controls for the player to enter their move or resign. When the game is over, this is replaced by a section showing the final status of the game (i.e. whether it was won or drawn, and who the winner was, if it was won). This section also has a button to start a new game.

To make sure all this appears as it should do, there’s an accompanying CSS file:

/* CSS file for Tic Tac Toe */


.board {
  display: flex;
  flex-wrap: wrap;
  height: auto;
}

.cell_container {
  width: 30%;
  height: 100px;
}

.cell_content {
  position: relative;
  top: 20%;
  left: 50%;
}

.centre {
  border-left: black solid 1px;
  border-right: black solid 1px;
}

.game_layer {
    margin: auto 25%;
}

.hidden {
  display: none;
}

.middle {
  border-top: black solid 1px;
  border-bottom: black solid 1px;
}

.warning {
  color: red;
  font-weight: bold;
}

The interesting thing here is the CSS that controls the layout of the board. This uses a CSS feature called Flexbox that makes it easy to create responsive grid-based page layouts.

The key principle is that a container that has its display property set to flex can control the layout of the block level elements it contains. You can see this applied to the board class.

By default, items with a flex container will be displayed in a horizontal row. You can change this using the flex-direction property. In this case I want to keep the horizontal flow, but I want to end up with a 3 x 3 grid. To do that I need to do two things. The first is to add a flex-wrap property to the flex container and set this to wrap. This forces child items to wrap to a new row when there is not room for them at the end of the current row. Next I set the size of the child objects so that I get the fit I want. In this case, I’ve set the cell_content class to have a width of 30%. This ensures that only three of them will fit into a single row.

When the page is rendered in the browser you can see the effect of this:

The page rendered in a browser showing how flex box can be used to get a simple grid-based layout.
A simple grid layout with flex box.

This initial UI is pretty ugly, but it does server the purpose of providing a simple harness to test the game logic as I add it.

Note that the dividing lines are created by applying a middle and centre class to the relevant cells, which draw top / bottom and left / right borders respectively.

The initial JavaScript allows two human players to play a game:

// Javascript file for Tic Tac Toe

var tictactoe = {
  board: [],  // Holds array representing board (set in resetGame function)
  // Lines is an array with the board positions representing the possible win lines
  lines: [
    [0, 1, 2],
    [0, 4, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 4, 6],
    [2, 5, 8],
    [3, 4, 5],
    [6, 7, 8]
  ],
  turn: 'O',  // A character representing the current player
  moves: 0,   // Number of moves made in the game

  // Initialise the game on start up (set up UI handlers and begin new game)
  initialiseGame: function() {
    document.querySelector('#quit').addEventListener('click', () => {
       tictactoe.quit(true);
    });
    document.querySelector('#new').addEventListener('click', this.resetGame);
    document.querySelector('#play').addEventListener('click', this.play);
    this.resetGame();
  },

  // Draw the current board based on the contents of the board array
  drawBoard: function() {
    for(let i = 0; i < 9; i++) {
      document.querySelector('#cell' + i).innerHTML = this.board[i];
    }
    document.querySelector('#turn').innerHTML = this.turn;
  },

  // Called when a player inputs a move. Validates the move, then updates and
  // redraws the board
  play: function() {
    let valWarning = document.querySelector('#valWarning');
    let posWarning = document.querySelector('#posWarning')
    let cellInput = document.querySelector('#selectedCell')

    valWarning.classList.add('hidden');
    posWarning.classList.add('hidden');

    let selectedCell = parseInt(cellInput.value);

    // Check player entered a valid cell number
    if (isNaN(selectedCell) || selectedCell < 1 || selectedCell > 9) {
      valWarning.classList.remove('hidden');
    } else {
      selectedCell--;

      // Check entered cell is empty
      if (tictactoe.board[selectedCell] != ' ') {
        posWarning.classList.remove('hidden');
      } else {
        // if valid cell has been entered, process the move
        cellInput.value = '';

        tictactoe.board[selectedCell] = tictactoe.turn;
        tictactoe.turn = tictactoe.otherPlayer();
              tictactoe.drawBoard();

        // Check for a won game, then for a draw
        if (tictactoe.checkForWin()) {
          tictactoe.quit(true);
        } else if (++tictactoe.moves == 9) {
          tictactoe.quit(false);
        }
      }
    }
  },

  // Checks if either player has achieved a line of three marks
  checkForWin: function() {
    for (let i = 0; i < this.lines.length; i++) {
      if (this.countMarks('O', i) == 3 || this.countMarks('X', i) == 3) {
        return true;
      }
    }
    return false;
  },

  // Counts the marks for the given player in the given win line
  countMarks: function(piece, line) {
    let count = 0;
    for (let i = 0; i < this.lines[line].length; i++) {
      if (this.board[this.lines[line][i]] == piece) {
        count++;
      }
    }
    return count;
  },

  // Called when the game ends. Displays the winner or a draw message and the
  // controls to start a new game
  quit: function(gameIsWon) {
    let winMsg = document.querySelector('#winMsg');
    let drawMsg = document.querySelector('#drawMsg');
    winMsg.classList.add('hidden');
    drawMsg.classList.add('hidden');

    if(gameIsWon) {
      document.querySelector('#winner').innerHTML = this.otherPlayer();
      winMsg.classList.remove('hidden');
    } else {
      drawMsg.classList.remove('hidden');
    }

    document.querySelector('#inPlayControls').classList.add('hidden');
    document.querySelector('#outOfPlayControls').classList.remove('hidden');
  },

  // Starts a new game, clearing the board, setting the first player to 'O' and
  // showing the UI for entering moves
  resetGame: function() {
    tictactoe.board = [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '];
    tictactoe.turn = 'O';
    tictactoe.moves = 0;
    document.querySelector('#inPlayControls').classList.remove('hidden');
    document.querySelector('#outOfPlayControls').classList.add('hidden');
    tictactoe.drawBoard();
  },

  // Returns the character of the player not currently in play
  otherPlayer: function() {
    return tictactoe.turn == 'O' ? 'X' : 'O';
  }
}

You’ll notice at the end of the HTML, there is a short bit of script that sets up an event handler to call the initialiseGame function once the DOM is loaded. I need to wait for this event, because the code will make multiple references to DOM elements.

This function simply sets up the event handlers for the three buttons in the game and then calls the resetGame function to start a new game.

The board is represented by a single-dimensioned array with nine elements. Initially these are all set to a space character, indicating an empty cell. As moves are made, these spaces will be replaced with either a ‘O’ or ‘X’ character.

To make a move in this version of the game, the player simply enters the number of the cell they wish to make their mark in. Both players will use the same text entry box to do this with the game informing them whose turn it is to play.

The game running in a browser showing that moves are made by entering the required cell number and pressing the Play button.
Players make a move using the text box and Play button.

You can see that in this example O has made their first mark in the centre cell. The prompt has changed to show it is X’s turn to play and that player is about to play into the top-left cell (1).

The user input needs to be validated in two ways. The first is to check that the user has entered a number and that number ranges between 1 and 9. In the HTML I have used an input element with a type attribute set to number. This will restrict the user to entering only characters that are used in numbers. However, the browser validation is not strong enough to rely on this entirely, and it will still allow floating point numbers, number with exponents etc.

On line 47 you can see that I first try to interpret the entry as an integer, using the parseInt function. This returns either an integer value or Not a Number (NaN). On line 50, I first use the isNaN function to check that an integer was parsed and, if so I then check that it is within the required range of 1 to 9. If this check is failed than a warning message is made visible on the page, asking the player to enter a valid number.

The game showing a warning prompt to the player to choose a valid cell number.
The warning if an invalid cell number is chosen.

The next check is to prevent the player from trying to overwrite the mark in an existing cell. Note that before I do this, on line 53 I decrement the entered value to bring it into the range used by the board array (0 – 8). This is one of those instances where, as a programmer, you need to avoid the temptation to make life easier for yourself at the expense of a good user experience. The simplest thing would have to been to number the board cells from 0 to 8, but that doesn’t make much sense to users who don’t have a programming background, so it’s better to use numbers that will appear logical to the user and then make the necessary adjustment in the background.

That being done, I simply check that the selected cell contains a space character. if so, it’s empty and available. If not, again another warning is made visible to ask the user to choose an unoccupied cell.

The game showing a warning if the player tries to play into an occupied cell.
The warning if a player choses a cell that is already occupied.

If all is well at this point, I clear the value in the input box, set the selected cell on the board to the character for the current player and redraw the board and the prompt.

Before the turn ends, I need to make two more checks, for a win condition and a drawn game. The check for a win condition makes use of an array, lines set on line 6, containing 8 sets of three numbers. These represent the numbers of the cells that constitute a possible win line. This array is used by the checkForWin function to see if either player has achieved a line of three.

In turn this function makes use of a countMarks function, which simply returns the number of marks for a given player on a given line. I’ve split this out into its own function as it will eventually be reused by the AI player when determining its best move.

The game showing a win being declared.
A win condition is detected and the game ends showing a suitable message.

Checking for a drawn game is made by keeping track of the number of moves. If this reaches 9 and neither player has won, then the game is drawn. Note that for this simple check to work, this check must be made after the win check, as it is possible that the win could have been achieved on the final move of the game.

The game showing a draw being declared.
A drawn game is detected and the game ends with a suitable message.

The quit function is called when a win or draw condition is detected or when a player chooses to resign. It takes a single parameter that is set to true if the game was won, or false if it was drawn. This simply determines which of two game outcome messages is shown. Note that, because this function takes a parameter, I’ve enclosed it in an arrow function when it is called as the event handler for the Resign button (see line 21).

Now that I have the basic functionality for two human players to play a legal game, in the next post I’ll look at adding the most interesting part of this project, the AI player.

Published inGamesProgramming

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *