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:
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.
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 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.
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.
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 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.
Be First to Comment