You can see an index of all the posts in this series: go to index.
If you want to view my progress to date, you can find the files from the end of the previous post here: Improving the AI player
In the previous post, I upgraded the AI player to use a combination of a variant of my initial AI routine and the popular Minimax algorithm. The AI player at its hardest level now presents a serious challenge, while still being easy to beat at the lower levels.
With that done I have a completely playable game, but there are a couple of issues. Firstly, it looks scrappy:
Secondly, human players can only make a move by entering the number of the cell they want to play to. Ideally I want a player to have a choice between that and simply clicking inside the cell they want to play to.
I’ll deal with the first problem, but just to set your expectations, I’m no graphic artist, so, while I can make improvements, the end result won’t look as professional as it could do in the hands of a good artist/interface designer.
I’m going to largely use CSS to improve the look of the game. Before I do that, I have to make some changes to index.html to add in some extra classes that I’ll need to reference.
<!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 type="module" src="js/tictactoe.js"></script> --> </head> <body> <div class="game_layer"> <h1>Tic Tac Toe<br>(or Noughts and Crosses)</h1> <!-- board --> <div class="board" id="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" class="hidden"> <p class="message">It's <span id="turn"></span>'s turn to play.</p> <!-- user input --> <input id="selectedCell" type="number"> <button class="button" id="play" disabled>Play</button> <button class="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="resultMsg" class="hidden"> <p id="winMsg" class="message"><span id="winner"></span> wins!</p> <p id="drawMsg" class="message">The game is drawn!</p> </div> <div id="newGameControls" class="box"> <h2>New Game</h2> <div id="playerSetup"> <div class="player"> <h3>Player O (goes first)</h3> <label for="playerOHuman">Human</label><input type="radio" name="playerO" id="playerOHuman" checked> <label for="playerOComputer">Computer</label><input type="radio" name="playerO" id="playerOComputer"><br><br> <label for="compStrengthO">Computer strength</label> <select class="button" id="compStrengthO"> <option value="0">Very weak</option> <option value="1">Weak</option> <option value="2">Average</option> <option value="3">Strong</option> <option value="4">Very strong</option> </select> </div> <div class="player"> <h3>Player X</h3> <label for="playerXHuman">Human</label><input type="radio" name="playerX" id="playerXHuman" checked> <label for="playerXComputer">Computer</label><input type="radio" name="playerX" id="playerXComputer"><br><br> <label for="compStrengthX">Computer strength</label> <select class="button" id="compStrengthX"> <option value="0">Very weak</option> <option value="1">Weak</option> <option value="2">Average</option> <option value="3">Strong</option> <option value="4">Very strong</option> </select> </div> </div> <button class="button" id="new">Start New Game</button> </div> </div> <!-- script to boot game once DOM loaded --> <script type="module"> import { initialiseGame } from './js/tictactoe.js'; document.addEventListener('DOMContentLoaded', function() { initialiseGame(); }); </script> </body> </html>
With that done, it’s simply a case of updating tictactoe.css:
/* CSS file for Tic Tac Toe */ body { font-family: Arial, sans-serif; } button { padding: 6px; border: none; font-size: large; margin: 10px 0; } h1 { text-align: center; background-color: #337788; color: white; border-radius: 20px; } h2 { color: #337788; } h3 { color: #337788; padding-bottom: none; margin-bottom: none; } #playerSetup { display: flex; justify-content: space-around; padding-bottom: 2em; } .board { display: flex; flex-wrap: wrap; height: auto; width: 80%; margin: 30px auto; } .box { border: 2px solid #337788; padding: 20px; text-align: center; } .button { background-color: #337788; color: white; } .button:hover { background-color: #4499aa; } .button:disabled { background-color: grey; } .cell_container { width: 30%; height: 100px; } .cell_content { position: relative; text-align: center; font-size: 4em; color: #337788; } .cell_id { background-color: #337788; padding: 2px; margin: 2px; width: 15px; text-align: center; color: white; } .centre { border-left: 2px solid #337788; border-right: 2px solid #337788; } .game_layer { margin: auto 25vw; } @media screen and (max-width: 1200px) { .game_layer { margin: auto 10vw; } } @media screen and (max-width: 800px) { .game_layer { margin: auto; } } .hidden { display: none; } .message { color: #337788; text-align: center; font-size: 1.5em; font-weight: bold; } .middle { border-top: 2px solid #337788; border-bottom: 2px solid #337788; } .player { padding: 10px; border: 1px solid #337788; } .warning { color: red; font-weight: bold; }
Most of this is simply providing new styles for elements that were completely unstyled in the previous version. Things worth noting are the use of flexbox for the game setup screen (lines 30-34), which allows the setup controls for the two players to sit side by side if there’s room, and the media queries (89 – 103) that adjust the size of the margins either side of the UI depending on overall screen width.
Here’s an example of the end result:
As I said, it’s not up to the standard that a pro designer could achieve, but I hope you agree it’s a vast improvement on the vanilla interface.
The final thing for me to fix is the modes of play available to human players. I don’t want to remove the ability to play by entering the number of a cell in the box and clicking the Play button, because that keeps the game accessible for players who can’t use a mouse or mouse equivalent. That being said, I do want players to be able to use a mouse to select a cell as an alternative means of input.
To achieve this, the first thing I need is to know where a player is clicking on the board. This entails a further change to index.html:
<div class="board" id="board"> <div class="cell_container" id="container1"><div class="cell_id">1</div><div class="cell_content" id="cell0"></div></div> <div class="cell_container centre" id="container2"><div class="cell_id">2</div><div class="cell_content" id="cell1"></div></div> <div class="cell_container" id="container3"><div class="cell_id">3</div><div class="cell_content" id="cell2"></div></div> <div class="cell_container middle" id="container4"><div class="cell_id">4</div><div class="cell_content" id="cell3"></div></div> <div class="cell_container middle centre" id="container5"><div class="cell_id">5</div><div class="cell_content" id="cell4"></div></div> <div class="cell_container middle" id="container6"><div class="cell_id">6</div><div class="cell_content" id="cell5"></div></div> <div class="cell_container" id="container7"><div class="cell_id">7</div><div class="cell_content" id="cell6"></div></div> <div class="cell_container centre" id="container8"><div class="cell_id">8</div><div class="cell_content" id="cell7"></div></div> <div class="cell_container" id="container9"><div class="cell_id">9</div><div class="cell_content" id="cell8"></div></div> </div>
The change here is that I’ve added a unique id to each of the “cell_container” divs. Note that these are numbered from 1 to 9 rather than 0 to 8 as the “cell_content_ divs are. You’ll see why I made that choice in a moment.
Now that I can identify those containers individually, I can upgrade ui.js which contains the code for the UI class:
// UI class // Encapsulates the user interface for Tic Tac Toe import { Game} from './game.js'; class Ui { #controls = null; #makeMoveHandler = null; #playerMoveEnabled = false; constructor(quitHandler, newGameHandler, makeMoveHandler) { this.#controls = { 'compStrengthO': document.querySelector('#compStrengthO'), 'compStrengthX': document.querySelector('#compStrengthX'), 'drawMsg': document.querySelector('#drawMsg'), 'inPlayControls': document.querySelector('#inPlayControls'), 'new': document.querySelector('#new'), 'newGameControls': document.querySelector('#newGameControls'), 'play': document.querySelector('#play'), 'playerOComputer': document.querySelector('#playerOComputer'), 'playerXComputer': document.querySelector('#playerXComputer'), 'posWarning': document.querySelector('#posWarning'), 'quit': document.querySelector('#quit'), 'resultMsg': document.querySelector('#resultMsg'), 'selectedCell': document.querySelector('#selectedCell'), 'turn': document.querySelector('#turn'), 'valWarning': document.querySelector('#valWarning'), 'winMsg': document.querySelector('#winMsg'), 'winner': document.querySelector('#winner'), 'board': document.querySelector('#board') } this.#makeMoveHandler = makeMoveHandler; this.#controls['quit'].addEventListener('click', quitHandler); this.#controls['new'].addEventListener('click', newGameHandler); this.#controls['play'].addEventListener('click', makeMoveHandler); let that = this; this.#controls['board'].addEventListener('click', function(event){that.selectCell(event, that)}); } // Draws the board drawBoard(board) { for(let i = 0; i < board.length; i++) { document.querySelector('#cell' + i).innerHTML = board[i]; } } // Displays the mark of the player whose turn it is to play displayTurn(mark) { this.#controls['turn'].innerHTML = mark; } // Displays the winner as the given mark displayWinner(mark) { this.#controls['winner'].innerHTML = mark; this.#showControl(this.#controls['winMsg']); } // Enables buttons so that a human player can make a move requestMove() { document.querySelector('#play').disabled = false; document.querySelector('#quit').disabled = false; this.#playerMoveEnabled = true; } // Shows the listed controls showControls(controls) { this.applyControlFunction(controls, this.#showControl); } // Show control #showControl(control) { control.classList.remove('hidden'); } // Hide the listed controls hideControls(controls) { this.applyControlFunction(controls, this.#hideControl); } // Hide control #hideControl(control) { control.classList.add('hidden'); } // Disable the listed controls disableControls(controls) { this.applyControlFunction(controls, this.disableControl); } // Disable control disableControl(control) { control.disabled = true; } // Clear controls clearControls(controls) { this.applyControlFunction(controls, this.clearControl); } // Clear control clearControl(control) { control.value = ''; } // Apply function to controls applyControlFunction(controls, funcToApply) { for (let i = 0; i < controls.length; i++) { funcToApply(this.#controls[controls[i]]); } } // Stops a player making moves with a mouse (or equivalent) disablePlayerMove() { this.#playerMoveEnabled = false; } // Validate numeric input isValidNumber(control, min, max) { let value = this.getNumericValue(control); return (!(isNaN(value) || value < min || value > max)); } // Gets a numeric value for a control getNumericValue(control) { return parseInt(this.#controls[control].value); } // Returns true if the indicated control is checked isChecked(control) { return this.#controls[control].checked; } selectCell(event, that) { if (that.#playerMoveEnabled) { let clickedElement = event.target; while (!clickedElement.classList.contains('cell_container')) { clickedElement = clickedElement.parentNode; } let elementID = clickedElement.id; let selectedCell = elementID.charAt(elementID.length-1); that.#controls['selectedCell'].value = selectedCell; that.#makeMoveHandler(); } } } export { Ui };
The first thing to note here is that there are two new private properties for the UI class on lines 8 and 9:
- #makeMoveHandler holds a reference to the function that is passed in to handle player moves, because the UI will need to call this directly when a click on the board is detected. This is initialised on line 32.
- #playerMoveEnabled is a flag that determines whether clicks on the board should be actioned or ignored.
You can see that the board has now been added to the array of controls on line 30. This is because the board is now interactive rather than just being a display feature. On lines 36-37 I set up a handler for clicks on the board.
One tricky thing about event handlers is that they change the meaning of this. For normal methods in the UI class, this will reference the instance of the class in which the code is running. However, in an event handler, this references the object that triggered the event. Because I need to be able to reference the class instance within the event handler, before I set up the event handler, I create a variable that on line 36, which is a copy of this while it is referencing the class instance. Then in the event handler on line 37, I pass the event object and that to the selectCell method, which processes the player’s click.
The combination of lines 36 and 37 creates something called a closure. A closure is a mechanism that gives an inner function access to an outer function’s scope. This means, when the event handler is called, it still has access to the local variable that. Closures can be a bit tricky to get your head round, but they are extremely useful in situations like this.
So let’s take a look at the selectCell method and see what that does. The first thing it does is check to see if it should do anything at all. The condition on line 135 stops the code from running if #playerMoveEnabled is false. This is necessary because there will be times, e.g. when a game has finished and the player has yet to start a new game, when we want to ignore any clicks on the board.
In an event handler, event.target references the object that triggered the event. The problem I have here, is that this could be any one of:
- the “cell_content” div that contains a player’s mark, i.e. “O” or “X”
- the “cell_id” div that contains the number in the top-left corner that identifies the cell
- or the “cell_container” div that contains both of the above divs plus the surrounding space
To get round this, I create a local variable called clickedElement on line 136 which references event.target in the first instance. I then set up a loop on lines 138 to 140. This interrogates clickedElement to see if it is a”cell_container” div. If it is, we’re done. If not, I set clickedElement to be the parent of the current object and try again.
One thing to note about this is that, with the current html, if event.target is not a “cell_container” div then its parent always will be, so I could have replaced the loop with:
if (!clickedElement.classList.contains('cell_container') { clickedElement = clickedElement.parentNode; }
However, the loop is a little more robust in that, if I were to add a further layer inside the cells at some point in the future, it would still work, whereas the simple conditional check might not.
Once I know I have the “cell_container” div, I simply get the final character of the id. This gives me a number between 1 and 9. Now all I have to do is put that number into the text box and call the function referenced by #makeMoveHandler, which effectively simulates clicking the Play button. You can see now why I numbered the cell container ids from 1 to 9 rather than from 0 to 8!
Doing things this way means the player can use either the original method of entering a cell number or they can just click on the cell and they can even switch methods mid game, since both forms of input are always active.
Note the disablePlayerMove method, which sets #playerMoveEnabled to false. This is used by tictactoe.js to prevent mouse selection of cells when it is not a human player’s turn, or once the game has ended (you might recall that cell selection with the text box is prevented because that control gets hidden when the game ends):
// if valid cell has been entered, pass the move to the game for processing if (valid) { ui.disablePlayerMove(); ui.disableControls(['play', 'quit']); ui.clearControls(['selectedCell']); processOutcome(game.processMove(selectedCell)); } } // Called when the game ends. Displays the winner or a draw message and the // controls to start a new game function quit(outcome) { ui.hideControls(['winMsg', 'drawMsg']); if(outcome == 'O' || outcome == 'X') { ui.displayWinner(outcome); } else { ui.showControls(['drawMsg']); } ui.disablePlayerMove(); ui.hideControls(['inPlayControls']); ui.showControls(['resultMsg', 'newGameControls']); }
You might have noticed that there is no corresponding enablePlayerMove method. This is because mouse control is automatically re-enabled by the UI class when the requestMove method is called (see line 62 of ui.js shown earlier in the post).
You can play a live version of the game.
So that’s it for Tic Tac Toe. For my next project, I’m going to create a real time game using the canvas API and a very useful function of the window object called requestAnimationFrame.
Be First to Comment