Michael Adsit Technologies, LLC.

Search terms of three or more characters

Refactoring a Project - Tic-Tac-Toe

The purpose of this learn more about the review and refactor loop involved in programming, using the implemented tic-tac-toe from the last lesson in addition to a brief talk about AI in programming, and why it hasn't been brought up sooner.

What leads to review and refactors of a project (or a part of one)?

There are many reasons that one may want to review a project, from being part of a larger team, to wanting to make sure there is nothing major seeming off (please note that there are various automatic tests that can be run to help with this - once you fix a problem, it is nice to never have to see it again), to formatting via a linter, to just wanting to see what new concepts you have learned that can be applied now. Many of these can lead to refactors, augmentations, or even scrapping parts of the project that no longer matter or reduce the overall usefulness of the product. For instance, if you are making an interface off of a set of assumptions, and start using it and realize that your assumptions were wrong and you want implement a redesign based upon new data and experience.

In a typical software development lifecycle, our git pushes will not be going to one branch but instead utilizing multiple branches following a strategy of some sort (depends upon the team), and then request to be pulled into the main codebase after a review (and possibly completing some changes that have been requested or else discussed due to the review). In many companies some form of agile development is utilized, which is designed to allow a flexible roadmap on the way to the final (or next iteration) of a product. Refactors may happen as part of technical debt that has occurred on the way, or needing to plug in something unexpected at the time of the initial code writing.

So, if you are working with others, more than likely you will be doing something along the lines of the loop below - though your own requirements may also change for a home project along the way too.

Application Development Lifecycle

You will notice that we have done everything in that but for the refactoring step as it was our first time through. However, last time we did put down some TODOs with questions that will lead to our new requirements.

A brief talk on AI

At the time of this writing, it seems that nearly everywhere I look I read or hear something about AI. While it continues to improve, in the realm of programming the training data that is fed in can often be very poor quality for complex use cases. Over the weekend I read an article someone wrote blaming an AI for causing them to lose about ten thousand sales opportunities, and the general feedback from other people was pointing out the other mistakes that allowed this to happen. It makes a great use case as to why you should understand the code and be able to review it like it is an interns prior to accepting it.

Human eyes on the AI output is important when it will affect actual real life things especially. One thing that many tech leads have noticed after a few years of AI being more generally available is that the code bases are getting more difficult to maintain as people do not bother to learn them and just have the AI create a function that already exists in many other places.

All of that being said, I just asked for a Tic-Tac-Toe game from ChatGTP (Prompt: Can you write me a tic-tac-toe game in javascript and html as one file?) and got the following which is certainly a working version, and with some refining we might be able to get one that aligns with what we want. If you want, you can put this code into your index.html, or another html file an browse to it from the live server (or even just off the file browser).

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5    <meta charset="UTF-8">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7    <title>Tic-Tac-Toe</title>
8    <style>
9        body {
10            display: flex;
11            justify-content: center;
12            align-items: center;
13            height: 100vh;
14            background-color: #f0f0f0;
15            font-family: Arial, sans-serif;
16        }
17
18        .container {
19            display: grid;
20            grid-template-columns: repeat(3, 100px);
21            grid-gap: 5px;
22        }
23
24        .cell {
25            width: 100px;
26            height: 100px;
27            display: flex;
28            justify-content: center;
29            align-items: center;
30            font-size: 2rem;
31            background-color: #fff;
32            border: 2px solid #ccc;
33            cursor: pointer;
34        }
35
36        .cell:hover {
37            background-color: #e0e0e0;
38        }
39
40        .winner {
41            margin-top: 20px;
42            font-size: 1.5rem;
43            color: #333;
44        }
45    </style>
46</head>
47
48<body>
49    <div>
50        <div class="container" id="board"></div>
51        <div class="winner" id="winner"></div>
52        <button onclick="resetGame()">Restart Game</button>
53    </div>
54    <script>
55        const board = document.getElementById('board');
56        const winnerDisplay = document.getElementById('winner');
57        let currentPlayer = 'X';
58        let gameBoard = ['', '', '', '', '', '', '', '', ''];
59        let gameActive = true;
60
61        const winningConditions = [
62            [0, 1, 2],
63            [3, 4, 5],
64            [6, 7, 8],
65            [0, 3, 6],
66            [1, 4, 7],
67            [2, 5, 8],
68            [0, 4, 8],
69            [2, 4, 6]
70        ];
71
72        function handleCellClick(event) {
73            const cell = event.target;
74            const cellIndex = Array.from(board.children).indexOf(cell);
75
76            if (gameBoard[cellIndex] !== '' || !gameActive) {
77                return;
78            }
79
80            gameBoard[cellIndex] = currentPlayer;
81            cell.textContent = currentPlayer;
82
83            if (checkWinner()) {
84                gameActive = false;
85                winnerDisplay.textContent = `Player ${currentPlayer} Wins!`;
86                return;
87            }
88
89            if (gameBoard.every(cell => cell !== '')) {
90                gameActive = false;
91                winnerDisplay.textContent = 'Game is a Draw!';
92                return;
93            }
94
95            currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
96        }
97
98        function checkWinner() {
99            return winningConditions.some(condition => {
100                return condition.every(index => {
101                    return gameBoard[index] === currentPlayer;
102                });
103            });
104        }
105
106        function resetGame() {
107            gameBoard = ['', '', '', '', '', '', '', '', ''];
108            currentPlayer = 'X';
109            gameActive = true;
110            winnerDisplay.textContent = '';
111            Array.from(board.children).forEach(cell => {
112                cell.textContent = '';
113            });
114        }
115
116        function createBoard() {
117            for (let i = 0; i < 9; i++) {
118                const cell = document.createElement('div');
119                cell.classList.add('cell');
120                cell.addEventListener('click', handleCellClick);
121                board.appendChild(cell);
122            }
123        }
124
125        createBoard();
126    </script>
127</body>
128
129</html>
130

After pasting this in, I was curious what prompts I would need to do to get one that is more in line with the features we added to our own version, and was able to get it to go much better than the last time I had tried to create a breakout game, but that was a few iterations ago. So, with these extra prompts I eventually ended up on the below.

  • Can we make the grid accessible?
  • Aren't grids supposed to be able to be navigated via the arrow keys?
  • Can we also allow the starting player character to be picked?
  • Can we have the aria-labels tell the row and column instead of cell index?
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5    <meta charset="UTF-8">
6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
7    <title>Tic-Tac-Toe</title>
8    <style>
9        body {
10            display: flex;
11            flex-direction: column;
12            align-items: center;
13            height: 100vh;
14            background-color: #f0f0f0;
15            font-family: Arial, sans-serif;
16        }
17
18        .container {
19            display: grid;
20            grid-template-columns: repeat(3, 100px);
21            grid-gap: 5px;
22            margin-top: 20px;
23        }
24
25        .cell {
26            width: 100px;
27            height: 100px;
28            display: flex;
29            justify-content: center;
30            align-items: center;
31            font-size: 2rem;
32            background-color: #fff;
33            border: 2px solid #ccc;
34            cursor: pointer;
35        }
36
37        .cell:hover,
38        .cell:focus {
39            background-color: #e0e0e0;
40        }
41
42        .winner {
43            margin-top: 20px;
44            font-size: 1.5rem;
45            color: #333;
46        }
47
48        button,
49        select {
50            margin-top: 20px;
51            font-size: 1rem;
52        }
53    </style>
54</head>
55
56<body>
57    <div>
58        <label for="starting-player">Select Starting Player: </label>
59        <select id="starting-player">
60            <option value="X">X</option>
61            <option value="O">O</option>
62        </select>
63        <button onclick="startGame()">Start Game</button>
64        <div class="container" id="board" role="grid" aria-label="Tic-Tac-Toe Board" style="display: none;"></div>
65        <div class="winner" id="winner" aria-live="polite"></div>
66        <button onclick="resetGame()" style="display: none;">Restart Game</button>
67    </div>
68    <script>
69        const board = document.getElementById('board');
70        const winnerDisplay = document.getElementById('winner');
71        const startButton = document.querySelector('button[onclick="startGame()"]');
72        const resetButton = document.querySelector('button[onclick="resetGame()"]');
73        const startingPlayerSelect = document.getElementById('starting-player');
74        let currentPlayer;
75        let gameBoard = ['', '', '', '', '', '', '', '', ''];
76        let gameActive = true;
77
78        const winningConditions = [
79            [0, 1, 2],
80            [3, 4, 5],
81            [6, 7, 8],
82            [0, 3, 6],
83            [1, 4, 7],
84            [2, 5, 8],
85            [0, 4, 8],
86            [2, 4, 6]
87        ];
88
89        function startGame() {
90            currentPlayer = startingPlayerSelect.value;
91            board.style.display = 'grid';
92            startButton.style.display = 'none';
93            resetButton.style.display = 'block';
94            winnerDisplay.textContent = '';
95        }
96
97        function handleCellClick(event) {
98            const cell = event.target;
99            const cellIndex = Array.from(board.children).indexOf(cell);
100
101            if (gameBoard[cellIndex] !== '' || !gameActive) {
102                return;
103            }
104
105            gameBoard[cellIndex] = currentPlayer;
106            cell.textContent = currentPlayer;
107            cell.setAttribute('aria-label', `Row ${Math.floor(cellIndex / 3) + 1}, Column ${cellIndex % 3 + 1}, ${currentPlayer}`);
108
109            if (checkWinner()) {
110                gameActive = false;
111                winnerDisplay.textContent = `Player ${currentPlayer} Wins!`;
112                return;
113            }
114
115            if (gameBoard.every(cell => cell !== '')) {
116                gameActive = false;
117                winnerDisplay.textContent = 'Game is a Draw!';
118                return;
119            }
120
121            currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
122        }
123
124        function checkWinner() {
125            return winningConditions.some(condition => {
126                return condition.every(index => {
127                    return gameBoard[index] === currentPlayer;
128                });
129            });
130        }
131
132        function resetGame() {
133            gameBoard = ['', '', '', '', '', '', '', '', ''];
134            gameActive = true;
135            winnerDisplay.textContent = '';
136            board.style.display = 'none';
137            startButton.style.display = 'block';
138            resetButton.style.display = 'none';
139            Array.from(board.children).forEach(cell => {
140                cell.textContent = '';
141                cell.setAttribute('aria-label', `Row ${Math.floor(Array.from(board.children).indexOf(cell) / 3) + 1}, Column ${Array.from(board.children).indexOf(cell) % 3 + 1}, empty`);
142            });
143        }
144
145        function handleArrowKeyNavigation(event) {
146            const cells = Array.from(board.children);
147            const currentIndex = cells.indexOf(document.activeElement);
148            let newIndex;
149
150            switch (event.key) {
151                case 'ArrowUp':
152                    newIndex = currentIndex - 3;
153                    break;
154                case 'ArrowDown':
155                    newIndex = currentIndex + 3;
156                    break;
157                case 'ArrowLeft':
158                    newIndex = currentIndex - 1;
159                    break;
160                case 'ArrowRight':
161                    newIndex = currentIndex + 1;
162                    break;
163                default:
164                    return;
165            }
166
167            if (newIndex >= 0 && newIndex < cells.length) {
168                cells[newIndex].focus();
169            }
170        }
171
172        function createBoard() {
173            for (let i = 0; i < 9; i++) {
174                const cell = document.createElement('div');
175                cell.classList.add('cell');
176                cell.setAttribute('role', 'gridcell');
177                cell.setAttribute('aria-label', `Row ${Math.floor(i / 3) + 1}, Column ${i % 3 + 1}, empty`);
178                cell.setAttribute('tabindex', '0');
179                cell.addEventListener('click', handleCellClick);
180                cell.addEventListener('keydown', (event) => {
181                    if (event.key === 'Enter' || event.key === ' ') {
182                        handleCellClick(event);
183                    } else {
184                        handleArrowKeyNavigation(event);
185                    }
186                });
187                board.appendChild(cell);
188            }
189        }
190
191        createBoard();
192    </script>
193</body>
194
195</html>
196

I also wanted to mention that I have been using codeium as a plugin for Visual Studio Code and have found it helpful for writing boilerplate code for me (and it is free to use). So - the basic conclusion I have come to is that utilizing an AI to help can be nice, but it does not replace the human mind for reasoning and requirement gathering as well as ensuring tests are there - there is a reason that it says, "ChatGPT can make mistakes. Check important info." This code introduces several thoughts and concepts we have not gone through yet, and these differences actually should play very well with our talk on refactoring.

Getting Ready to Refactor

Since I feel like I kind of blasted a lot of stuff out, lets get started with the review and refactoring by completing the creation of a new branch as was mentioned earlier. There are several ways to do so including the command line (git branch <branch name> and git checkout <branch name> or git checkout -b <new_branch_name>) or a graphical method in Visual Studio Code and even directly on GitHub. Because we are in Visual Studio Code, we will go over the graphical way inside of it. In the lower left you should see something labeled main, please click that.

main branch in lower left

This should cause a dropdown to appear at the top of the screen asking to create a new branch. Usually, you want to create it from main if you are not already on that branch - the Create new branch option is perfectly fine if you were already there.

Branch Selection Options

You will notice in my screenshot that several branches already exist. That is because I have been creating them at the end of each lesson to bookmark the point in time rather than have to go to a specific commit. The repo is more simple to explore then while maintaining our progress. Anyway, please select Create new branch and then give it a name, I chose refactoring in the above, and then hit Enter as requested. Now, in the lower left, you can see refactoring (or your new name if different) there along with a little cloud push symbol. If you click the cloud, your branch will now appear on GitHub as published. You can also use the dropdown on GitHub to create the branch instead, then switch to the origin/<branch name> in Visual Studio Code.

GitHub Branch Dropdown

Now, if something goes horribly wrong in our refactoring we can change back to main and easily see the differences.

Figuring out what to refactor

So, as mentioned in the loop, we need to do some more requirements gathering usually - though we could use the requirement of tech debt which is what I initially was planning on. So, lets look at the questions we added to the docs/mechanics/README.md last time and pick one or two. As we now have our code and the sample code from the AI, we can discuss the following two questions fairly well.

  • What should the final display look like?
  • What should be refactored (the code changed to be more readable or efficient)?

Since we now have two sets of code to play with and explore (the known codebase we already created, and an ai generated one) let us decided what we like about both - this will be utilized as adding a second perspective and allow digging into some more complex decisions. Originally I was going to introduce some of these elements differently, but I feel this makes a great starting point.

For playing with this, I started the live server and copied the ai-output version to a new file called ai-generated.html and can then access it via 127.0.0.1:5500/ai-generated.html.

Refactoring Question - What should the final display look like?

While there are many ways to display things, for this next iteration we will say that whatever we pick is good enough for us. To start, let me show the flow/graphics of the other version.

First, there is the selecting the starting player.

Select Starting Player - AI Version

Next is the main display. It did not show who is the current player, but does display a grid and allow us to interact with it.

Playing Grid Filled Out - AI Version

The Restart Game button goes back to the main screen.

This version used many concepts and items we have not yet learned, but for sure there are a few things that I was planning to bring up as part of this lesson, so that is nice.

First, our initial dialog that you click Cancel for O or Ok for X is a blocker to the page, and not really that great of an experience. This dropdown is a step in a good direction, but maybe it should be disabled along the way. Also, I love that the screen is centered, and having larger boxes is also nice. However, I want to keep the current player and also the color scheme that I do have as it kind of pops out and feels more game like. Also, it might be nice to have the Button and Starting Player Drop Down look a bit more modern, and we might want to explore what is available.

Research User Interface Choices

Since we have decided that we for sure want to change up the way that the starting player is chosen, we would like to do a little bit of looking around before deciding on our next design iteration. We should start by digging into the select element that was introduced in the ai-generated code, by reading a bit more on both w3schools and the mdn web docs so as to get some more examples. Reading through the mdn docs, we see some potentially difficult to deal with styling warnings and an article on advanced form styling as well as building custom form controls. For where we are in the series so far, this is a very complex thing to do and we want to avoid adding too many new things at once. As such, we decide that if we do choose the select, we will keep it as is.

Through the reading we can discover that buttons are easy to customize, so we might want to do that. Additionally, we can take a quick look at the w3schools HTML Form Elements section as we have found that both are parts of forms, to check if anything else sticks out.

Make a Decision

After some more thinking, we decide that we really want to use a styled button. Additionally, we would like to keep things on one screen, and given the knowledge that we already have, decide to go for a simpler design utilizing stylized buttons over the select.

So, we will end up with a similar color scheme to the original version, make the grid bigger, and make a section that has the phrase Start New Game and the two buttons X Begins and O Begins. If we are ambitious or part of a larger team with a designer, we (or someone on the team) would use some sort of visual design tool typically to mock up a design, but that is outside of our current scope. Instead, we will be combining aspects from each of the code base's css and digging a bit deeper to find out some more, and using a quickly done mock made using Gimp below for the initial visualization. In future lessons, we may dig into Figma or Penpot or something similar with a design system, but for now that is way overkill. In fact, for many personal things I just sketch it on a whiteboard to get general feel.

Basic Idea Sketch

We will play with centering the text and buttons as well when we get to to actually implementing the design and go from there.

Refactoring Question - What should be refactored (the code changed to be more readable or efficient)?

This should probably have been put into the document as what code should be refactored, but again having two code bases to look at gives us some discussion around this.

Quick note about for loops

One thing to note is that both code bases have sections in a format of for (let i = 0; i < 3; i++), which we did not explicitly cover. We have gone over using forEach before with an array, this format utilizing a for keyword is saying to go through the code so many times with a different variable available each time (in this case, it will do 0, 1, and 2). There are also while loops which we will save for a different day. So the basic structure of a for loop is for (variable initialization or reuse; continue going until when condition; variable incrementor). In most cases with a for loop, you are wanting to go so many times, and start with a variable initialized to 0. Traditionally, if you will have embedded for loops, the variables go i, j, x, y and then whatever you want. However, it is also a good practice to give a meaningful name if possible, so we may do that as a part of the refactoring just to make it easier to read.

Back to the question

If we start by reading through the basic HTML file, we will notice that the AI version doesn't have all of the cells defined directly, which is nice so we would like to maybe use something similar as we do already utilize a for loop, so cleaning that up would be nice - and more importantly, will introduce less room for human error. However, the choice to use div tags as clickable buttons rather than the button tag from an accessibility point I don't enjoy as much. Additionally, if we look up some of the tag attributes produced (such as role="gridcell") we can see that this is bad practice according to the mdn web docs so while we will want to utilize the generation method, we will stick with the table and button structure rather than the div structure proposed. With that decision, we will also keep the same navigation code we already had in play for accessibility. By keeping that in a separate file, we also maintain some separation of concerns in the code - trying to keep each bit of code focused on one thing.

When we move towards the JavaScript, one major difference is how the game board is structured. In our initial version, it is an array of three arrays of three characters. Here it is one array of nine characters. This leads to less embedded loops, but a need for more math thoughts. In college the second was the preferred way because of less memory consumption and technically better Big O complexity since you are not doing multiple loops. In the real world, typically the easier code to read through is the better since you will be spending a lot of time re-reading the same code as the base gets more complex, and you want to optimize when you notice slow or costly areas.

On that note, we should be able to redo the way areThereOpenSpaces works so that we don't need to do a loop, but instead check if there have been nine moves yet with a counter, and can either save the winner or force that count to nine if we want to force stop using just one condition without re-loading the winner. We will experiment with that, and I will leave it to you the reader to decide which method seems easiest to read. Know that the less loops you run, the faster a program generally will go, as such we will try adjusting the winner check to not check every possible solution. Eventually you learn data structures and various techniques to make things more efficient as needed, but for now we want to focus on the fundamental coding habits.

Finally, we will take a quick look for new CSS ideas from the AI code and see a few things that we will want to use. For our final design, we do want to have everything centered we decided, and also will want the background to be fully black so we will move the background-color from the div to the body.

Doing the Refactoring

Let us start by doing the HTML and CSS to match our new design thoughts.

Inside of the <div> in the index.html file, we should update to the following.

1<h1>Tic-Tac-Toe</h1>
2<p id="playerTurn"></p>
3<table>
4</table>
5<h2>Start New Game</h2>
6<button onclick="newGame('X')">X Begins</button>
7<button onclick="newGame('O')">O Begins</button>
8

Note that our table disappeared! We will bring that back now by editing the JavaScript main.js file. We will edit the createBoard function that was part of the AI generated structure to match our own tags. Additionally, comments will be added to explain more of what is going on.

Add the following between the makeMove and newGame functions.

1// Function to create the innards of the table displaying our game board
2function createBoardTableCells () {
3  // Grab the table element
4  const boardTable = document.querySelector('table')
5  for (let row = 0; row < 3; row++) {
6    // create three rows
7    const tableRow = document.createElement('tr')
8    tableRow.setAttribute('role', 'row')
9    for (let col = 0; col < 3; col++) {
10      // and three columns
11      const tableCell = document.createElement('td')
12      // while we were researching, we found since it is td already we don't need to do this
13      // however the accessible-table-grid.js still needs gridcell for looking things up
14      // we could make it look for td instead, but will just add the gridcell role
15      tableCell.setAttribute('role', 'gridcell')
16      // make a button for this cell
17      const button = document.createElement('button')
18
19      // add button attributes of type and onclick to match what was there before
20      button.setAttribute('type', 'button')
21            // no tabbing directly to the button, but instead to the table cell
22      button.setAttribute('tabindex', '-1')
23      // want to still be able to click and make something happen
24      button.setAttribute('onclick', `makeMove(${row}, ${col})`);
25
26      // attach the button to the table cell
27      tableCell.appendChild(button)
28      // attach the table cell with button to the table row
29      tableRow.appendChild(tableCell)
30    }
31
32    // attach the table row to the table
33    boardTable.appendChild(tableRow)
34  }
35}
36
37createBoardTableCells()
38

This will generate the table cells programmatically, rather than by copy/pasting and editing. One thing to note is that, while we have it designed as a game of three by three, one of the choices we could have considered was making the size able to be changed, then we would want to have a variable to account for that. Additionally, when you load now the accessible-table-grid.js will be having some errors. Please change the order in the index.html of the script tags, and also adjust the following line from accessible-table-grid.js.

Line 5: const trs = document.querySelectorAll('table tbody tr') becomes const trs = document.querySelectorAll('table tr')

This may have been a glitch introduced sometime after the initial testing of it last time, not 100% sure as it appears that we did not have the tbody anywhere in the last lesson, so I apologize for that if this is the case. On the plus side, this does show a great example of needing to refactor for bugs being found.

Now, lets adjust the newGame function, and get rid of the confirm dialog that we have been using so that refreshing the page does not require us to interact more and we can focus on the CSS after that. Note that in the HTML we added the X Begins and O Begins buttons to call new game with an argument of either X or O. So, we will need to add that argument and a way to check what index that is - happily there is the indexOf function that is perfect for this.

To make this work use the following newGame function (and initial call to it). Note that, if you were not to update the newGame call at the bottom, it would still work because of the check for -1. This is a very commonly used pattern, and you could try calling from the console to see what happens with a bad value.

1function newGame (startingCharacter) {
2  initializeGameBoard()
3  currentPlayerCharacterIndex = playerCharacters.indexOf(startingCharacter)
4  // make a default first player if not passed in
5  if(currentPlayerCharacterIndex === -1) {
6    currentPlayerCharacterIndex = 0
7  }
8  displayPlayerTurn()
9  matchDisplayBoardToGameBoard()
10}
11
12newGame('X')
13

Now would be a good time to push to GitHub, as we have done several small changes that we don't want to lose. We made the changes for the HTML to work and the supporting JavaScript changes for the new structure.

Now, we can start on the styling. First, lets make the whole background be black instead of just the div. In our main.css we want to change the div section to be body.

Next, we want to have everything centered in the div. This is much easier to do than it used to be now that we have something known as flexbox layout (link is to one of my favorite summaries of the system). This is something that has made responsive design much nicer as well, as it is designed for growing and shrinking components with screen size. Anyway, for this project what we discover when playing is that it will be ok to simply add the following div section to our CSS, since we only have one div.

1div {
2  display: flex;
3  align-items: center;
4  flex-direction: column;
5}
6

Next we wanted to make the center squares a bit bigger, which we can do by editing the table button section. While we will keep it sized with REM's still, we would like to update it so that it can be between three and four time's larger. We also want it to be able to change based upon the screen size, and still look ok. As such, we will be utilizing the css clamp and min functions to change the size with the screen, maxing out at one third the screen size (if your screen can only show 4.5 characters, we probably have other issues in the display...). Additionally, we will utilize the flex box stuff we just learned about.

1table button {
2    display: flex;
3    justify-content: center;
4    align-items: center;
5    width: clamp(1.5rem, 5rem, min(33.33vw, 33.33vh));
6    height: clamp(1.5rem, 5rem, min(33.33vw, 33.33vh));
7    font-size: clamp(1rem, 4.5rem, min(30vw, 30vh));
8    text-align: center;
9    padding: 0px;
10}
11

Next, we will want to adjust the buttons in the div.

1div > button {
2  background-color: lightgreen;
3  color: black;
4  border: none;
5  padding: 10px;
6  border-radius: 5px;
7  margin-top: 10px;
8  width: clamp(4.5rem, 15rem, min(100vw, 100vh));
9  font-size: 1.25rem;
10}
11

Note that we use a > symbol before button to ensure that only direct children nodes of the div have it applied.

You may try without it if you would like to see the effects.

And finally we will want to add the h2 selector next to the h1 as they currently both just use a color change.

1h1, h2 {
2  color: lightgreen;
3}
4

At this point we have a design similar as seen below to compare with our mock up.

Final Visual Design

So, we probably want to push to GitHub again before looking at the other refactoring bits in the JavaScript.

We had talked about refactoring a few things, including the areThereOpenSpaces which can be redone by making a counter go up every time a move is made, instead of having to scan the full board every time. Let us add a variable after the gameBoard called movesMade.

1// How many moves have been made in a game
2let movesMade = 0
3

Then, after making the move and before changing characters in the makeMove section, we will increment the value.

1// let move counter go up
2movesMade++
3

Now we can edit the areThereOpenSpaces function to contain just return movesMade < 9 - as this is used in only one space, we may consider refactoring it out entirely. However, either way, we still need to reset the movesMade = 0 inside of the newGame function, or we will have issues after the first nine moves.

1// reset how many moves have been made
2movesMade = 0
3

We had also talked about making the check for a winner be a little simpler and not looping through every possible solution.

For that, we will add the variable for winner to start, just as we did for movesMade, lets do it right below the movesMade and also in the newGame area

1// Initialize a winner variable
2let winner = false
3
4// inside of newGame
5    // reset the winner
6    winner = false
7

Realizing that canMakeMove is defined by open spaces and no winner, lets get rid of areThereOpenSpaces by putting the logic into canMakeMove and also update canMakeMove just to see if there is a winner set. We will be doing a check for the winner at the time of the move and setting it if there is one in the future.

1// check if we are able to make any more moves, if not the game is over
2function canMakeMove () {
3  return movesMade < 9 && !winner
4}
5

Next, we need to update winner whenever a winner comes back from checkForWinner during a make move. Just before changing players we can add this.

1// check if that move was a winner
2winner = checkForWinner(rowIndex, columnIndex)
3

And we can redo checkForWinner to accept the row and column as follows, getting rid of the loops. Note that we could do diagonals based upon what position was played, but I will leave that as an activity for the readers who wish to do so.

1// function to check all ways for a winner
2function checkForWinner (rowIndex, columnIndex) {
3
4  // check if we have a winner in the given row
5  const rowWinner = checkRowForWinner(rowIndex)
6  if (rowWinner) {
7    return rowWinner
8  }
9  // check if we have a winner in the given column
10  const columnWinner = checkColumnForWinner(columnIndex)
11  if (columnWinner) {
12    return columnWinner
13  }
14
15  // then the diagonals
16  return (
17    checkLeftToRightTopToBottomDiagonalForWinner() ||
18    checkLeftToRightBottomToTopDiagonalForWinner() ||
19    false
20  )
21}
22

Finally, we must remove the line const winner = checkForWinner() from the displayPlayerTurn function in order to ensure that we are checking the right winner variable, as the local version overrides it and that now does not know what position to check.

Now that we have completed the refactors that we wanted to, we can go to the mechanics/README.md and cross them off for now using ~~ to strike through. Additionally, I think we can update the answer the last question as well.

1- ~~What should the final display look like?~~
2- ~~What should be refactored (the code changed to be more readable or efficient)?~~
3- Does it count as either player starting in the status that it is currently in?
4  - Yes it does.
5

This leaves us with a few extra things to think about should we choose to. Now that our changes are over, we want to push to GitHub.

Getting your changes to the main branch

When you go to GitHub, you will see something like the following on your project page.

Compare & Pull Request Example

Your changes won't be there in the main branch either. As mentioned, this is part of the more normal flow of things, so let us click the big Compare & pull request button to see what happens.

I prefilled in a description before taking a screenshot, but you should see a screen like below (and if you keep scrolling down, you will see all of the differences between this branch and the main, or what we changed this lesson).

Open a pull request screen

Some important things to note right now include the title, description, branches (base: main and compare:refactoring) as well as the Able to merge status. Basically, we are telling it we want our changes here to go into the main branch, and it is saying that is doable without needing to do anything special. If you are on a team, you would usually select the reviewers and follow any other labeling guides agreed upon by the team.

Below the description, you can see what commits were made over time, as well as all of the changed files and what changed. Once you do a quick review, you can click the Create pull request button.

You should then see a screen similar to the following.

The actual PR Screen

This is where people are typically going to be sent if they are requested as a reviewer, and often you will have rules in play to make sure it is safe to go (we don't have any here right now as we aren't in that portion of learning yet). As a reviewer, I would usually read the description (and often would have links to tickets somewhere that describe why the PR is being created). Additionally, I would look through the Files changed tab and click to leave feedback where-ever I wanted to. So, for instance, lets say that that I realized I still have the ai-generated.html file around, and we don't actually want it included. Then, I would click the message bubble in the upper right of that file area and leave a message as below, and start a review.

A quick review start

Then click finish review and request changes. However, as the one who made the PR I can't do so, just comment. Anyway, we can remove that file and push to GitHub.

Outdated comment
.

Now the comment shows as outdated, but usually you would reply and then let the other person mark it as resolved. Or have more conversation if needed before resolving. After it is resolved, someone in the team would click on the Merge pull request button (I will do it myself as just me, but hopefully that gives a general basic flow idea for normal teams). Finally, you could delete the branch if you want to (I am using branches to link to, so I won't, but typically in a project you need to properly remove them or you get an over abundance as time goes on)

Merge Complete

Now you can change branches by clicking the branch name in VS Code, go to main, and tell it to synchronize with the button next to the branch names, or type in git pull to your terminal.

Conclusion

In this lesson we were able to complete a major part of any software development's lifecycle - the first refactoring. While we left some things as open questions to explore in the future, we were able to make things look a little nicer and remove some extra looping from our project. Additionally, we talked a bit about AI and why we want to have a solid understanding of the code before just blindly accepting it. The idea of visiting the docs for each new thing has been reinforced, and we also were introduced to git branches a little bit. This all sets us up to feel confident to start experimenting a bit more. We also were able to compare two different approaches to a similar problem and discuss some of the pros and cons of each.

You may view the current code on GitHub or StackBlitz.

Prior - Implementing a Project - Tic-Tac-Toe

Tags: Practical Programming, Tic-Tac-Toe, JavaScript, Table, HTML, CSS, Refactoring, Pull Requests, Flexbox, AI

©Michael Adsit Technologies, LLC. 2012-2024