📜 ⬆️ ⬇️

Developing Tic-Tac-Toe on native javascript

Hello everyone, and good day, Habravchane! Being on vacation, in order to escape from the routine and work processes, I decided to entertain myself with something and write something.

What to write? I decided to choose native JavaScript in order to improve my skill in one of the most ambiguous programming languages. What to write? Although I am engaged in web development, but I have long felt a love for GameDev, I’m a creative person, what can you do. Therefore, I stopped at one of the simplest games, tic-tac-toe.

I did not turn on the stopwatch while sitting down at these works, but recalling the process, I think I spent 15-20 hours on the game. I didn’t follow the time, so I can be wrong. I spent an hour or two a day, no more. I tried to get maximum pleasure from the process.

Pure javascript, and only hardcore! I did not use any libraries, and did not look into other implementations of this game, so as not to confuse my own vision. Therefore, I apologize in advance, in some places decisions may be far from ideal, but I am ready for dialogue, criticism, or just advice.
')
The whole game rests on three JS files, on one style file, and on index.html. Plus a directory with three images (cross, zero, background). The designer is okolonulevo with me, so I don’t strongly kick for design.

So, the three JS files, the heart of the game, are main.js, ai.js, and helpers.js. With your permission, I will tell only about two, because There is nothing particularly interesting in helpers.js; auxiliary and trivial functions are described there. You can see it yourself, I'm ready for their criticism too.

One more note, I did not set myself the goal of cross-browser compatibility, everything was checked by me on the last Chrome. Some features of ES6 are also used, so there may be problems with browsers unfriendly to it.

All js-code in three files fit in 272 lines (at the time of this writing). The main.js file contains the main code, and the ai.js file contains the implementation of the AI ​​similarity (the language does not turn to call it simply AI, so let me continue to call it FDI).

We proceed to the analysis.

A small tour of the variables. The only thing I will review from helpers.js:

// Sides player and AI var player; var ai; // Who goes first var first_run; // Battle blocks in the game var blocks = document.getElementsByClassName("block"); // Collections win lines for points var win_lines = [ ["1","2","3"], ["4","5","6"], ["7","8","9"], ["1","4","7"], ["2","5","8"], ["3","6","9"], ["1","5","9"], ["3","5","7"], ]; 

I apologize for my broken English in the comments to the code. The variables player and ai determine what sign the player plays and FDI, who has a cross, and who has a toe. The variable first_run determines who goes first. The variable blocks contains an array of html elements, so-called. cells on the playing field. Well, the variable win_lines is a two-dimensional array of winning lines, the collection of one of which is the goal of the game.

Start main.js:

 Array.from(blocks).forEach((element) => { element.addEventListener("click", function() { if (element.classList.contains(player) || element.classList.contains(ai)) { return; } var ai_count = countPoints(ai); var player_count = countPoints(player); if (first_run == "player" && ai_count < player_count) { return; } if (first_run == "ai" && ai_count == player_count) { return; } setImg(this, player); var win = identifyWinner(player); if (win) { endPlay("win"); window.clearInterval(monitoringSteps); } }); }); 

Let's agree, signs I will call crosses or noughts.

We iterate over the array of elements and hang each click of the listener on each cell of the playing field. By clicking on the cell, if there is no sign in it, we put a player's mark. On lines 9-10 we count all the signs and the player and FDI. Lines 12 through 17 help us control whose first move and the sequence of moves. So that the player could not resemble in turn. On line 19 is the function that puts the player's mark in the cell he clicked on, this procedure is described in helpers.js nothing interesting. In the end, we determine whether the player made the move. If so, the game is over. The indentifyWinner and endPlay functions are also described in helpers.

 var originally_points_player = 0; var monitoringSteps = setInterval(() => { var player_points_count = countPoints(player); if (player_points_count > originally_points_player) { originally_points_player = player_points_count; var empty_blocks = emptyBlocks(); if (empty_blocks.length > 0) { // run enemy var favorite_run = selectFavoriteRun(empty_blocks); if (favorite_run > 0) { var block_for_run = document.getElementById(favorite_run); } else { var random_index = Math.floor( Math.random() * (empty_blocks.length) ); var block_for_run = empty_blocks[random_index]; } setImg(block_for_run, ai); // end run enemy var win = identifyWinner(ai); if (win) { endPlay("lose"); } } } }, 2000); 

A code that tracks a player’s move and transmits FDI. Line 1, the variable expected_points_player stores the initial number of player characters. Initial - meaning, before his last move. On the 4th line we count the player’s marks, the 6th line, if their number has increased, then the player has made a move. We pass the initiative to FDI. We select empty blocks (cells on the playing field) and, if they exist, give the right to move FDI. The selectFavoriteRun () function, into which we pass empty cells, is described in ai.js, we will consider it later. In the meantime, I will say that its essence is in determining the most profitable move for FDI. The function returns either zero if the profitable move is undefined, or the ID number of the cell in which the FDI sign should be placed. If the returned value is greater than zero, select the block-cell for the move; if less, choose a random block-cell from the empty ones.

And on line 21 we put the sign of FDI on the playing field. From line 24 to line 27, we determine whether the move has made FDI.

 setInterval(() => { var done = document.getElementById("inscription").innerHTML.length == 0; if (emptyBlocks().length == 0 && done) { endPlay("draw"); } }, 1000); // Code for select side in the game var wrapper_button_select = document.getElementById("wrapper-button"); var button_select = wrapper_button_select.getElementsByTagName("button")[0]; button_select.addEventListener("click", function() { selectSide(); // Who goes first if (player == "cross") { first_run = "player"; } else if (player == "zero") { first_ai(); first_run = "ai"; } }); // Button "Again?" var button_again = document.getElementById("info-again"); button_again.addEventListener("click", function() { location.reload(); }); 

Further in main.js less interesting things. Here we first monitor the situation of a draw. If the cells have run out and the winner has not been determined, then a draw. The variable is done so that the definition of a draw does not occur before the start or after the end of the game.

Next, the code for selecting the player’s side and the code for the “Again?” Button.

Moving to ai.js, probably the most interesting. While he is poor and consists of only 44 lines. But there are thoughts and ideas for him, the vacation is over and everything comes down to ...

 var selectFavoriteRun = (empty_blocks) => { var points_player = document.getElementsByClassName(player); var ids_player = getIdsArray(points_player); // Do not to give player win var favor_run_no_win_player = determiningPlaceForRun(ids_player); var points_ai = document.getElementsByClassName(ai); var ids_ai = getIdsArray(points_ai); // Run for win AI var favor_run_win_ai = determiningPlaceForRun(ids_ai); return (favor_run_win_ai > 0) ? favor_run_win_ai : favor_run_no_win_player; }; 

Previously seen selectFavoriteRun function. So far, the determination of a profitable move for FDI is based on two principles. The first is if there is a move that will allow FDI to win, well, so we do it. If this is not. Then the second principle comes into force, if there is a danger that the player will win by the next move, by all means try to prevent him from doing so. If such a situation is not expected, we return zero. And let the random decide where to go.

So, 3-4 lines, we define all player signs on the field. We take aydishniki of these cells and pass to another function determiningPlaceForRun, which we consider below. We obtain from the function either an idish of a cell that is profitable for the stroke, or zero.

In lines 8 through 11, we do the same with the signs of FDI, to determine the possible victory. Well, we return the result. If there is an option to win, return it, no, then return the course for not allowing the player to win.

 var determiningPlaceForRun = (array_elements_points) => { var favorite_run = 0; win_lines.forEach((positions) => { var points_in_row = 0; var point_for_win = 0; for (var i = 0; i < array_elements_points.length; i++) { if (positions.indexOf(array_elements_points[i]) != -1) { points_in_row++; } if (points_in_row == 2) { point_for_win = positions.diff(array_elements_points)[0]; var is_cross = document.getElementById(point_for_win).classList.contains(player); var is_zero = document.getElementById(point_for_win).classList.contains(ai); if (is_zero || is_cross) { point_for_win = 0; } } } if (point_for_win > 0) { favorite_run = point_for_win; } }); return favorite_run; }; 

Well, the most terrible function is the definition of a cage for a move. The principle is the same for finding a winning option and for not allowing a player to win. In the cycle we go through the list of winning lines. Putting two variables points_in_row, point_for_win. The first to calculate the same type of characters in the lines of victory, the second for a potentially profitable move.

After we run the nested for, which goes through everything, already standing signs on the field. The condition on line 8 checks if there is a sign in the winning line. If there are two such signs. It means there is an opportunity to win or lose (depending on whose signs we check). At the 12th line we assign the remaining square from the winning line, variable for a profitable move. From line 13 to line 17 we are reinsured to check if the cell is free. If not, cancel the profitable move. (Actually, due to non-optimality of logic, there is no way without this test. Since we check for the presence of only one type of characters in the cells, at the same time the “type-free-profitable” cell may already be occupied by a sign of another type).

In the end, if the profitable move is greater than zero, it will return, otherwise a zero will fly out of the function.

That is until my primitive Tiktaktoy. In the future, there are plans to deepen in ES6 and zayuzat it in full power. Refine ai.js so that FDI does not use the randomization during the course, but immediately seeks to win. As well as the implementation of the levels of difficulty of the game. But it all comes down to time. Thank you all for your attention and kindness!

Link to GitHub project.

Source: https://habr.com/ru/post/347670/


All Articles