Tic-Tac-Toe native JavaScript development
Hello everyone, and good day, Khabravchians! While on vacation, in order to escape from the routine and work processes, I decided to entertain myself with something and write something like that.
What to write on? I decided to choose native JavaScript in order to pull up my skill in one of the most controversial programming languages. What to write? Although I’m doing web development, I have long been fond of 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 for this work, but remembering the process, I think I spent 15-20 hours playing the game. I didn’t especially follow the time, so I can be wrong. Spent an hour or two a day, not more. I tried to get the most out of 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 that in some places decisions may be far from ideals, but I am ready for dialogue, criticism, or simply 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 near-zero with me, so I ask you not to kick much for the 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 you only about two, because helpers.js has nothing particularly interesting, it describes auxiliary and trivial functions. You can watch it yourself, I am also ready for their criticism.
Another note, I did not set the goal of cross-browser compatibility, everything was checked by me on the latest Chrome. Some features of ES6 are also used, so there may be problems with browsers that are not friendly 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 similarity of AI (the language does not dare to call it just AI, so let me continue to call it FDI).
We proceed to the analysis.
A small digression on the variables. The only thing I'll cover from helpers.js:
I apologize for my broken English in the comments to the code. The player and ai variables determine what character the player and FDI play, who has a dagger and who has a toe. The variable first_run determines who goes first. The blocks variable contains an array of html elements, the so-called cells on the playing field. Well, the win_lines variable is a two-dimensional array of winning lines, the collection of one of which is the goal of the game.
Start of main.js:
We agree, signs I will call the crosses or zeros.
We sort through the array of elements and hang on each cell of the playing field of the listener on a click. By clicking on the cell, if there is no sign in it, put the player’s sign. On lines 9-10, we count all the signs of both 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 walk in turn. On line 19, the function puts the player’s sign in the cell by which he clicked, this procedure is described in helpers.js nothing interesting. At the end, we determine whether the player made the winning move, if so, the game is over. The functions indentifyWinner and endPlay are described there, in helpers.
A code that tracks the player’s progress and conveys FDI initiative. Line 1, the variable original_points_player stores the original number of player characters. Initial - meaning, until his last move. On the 4th line we count the player’s signs, 6th line, if their number has increased, then the player has made a move. We pass on the FDI initiative. We select empty blocks (cells on the playing field) and if they are, we give the right to move FDI. The selectFavoriteRun () function, into which we pass empty cells, is described in ai.js, which we will consider 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 advantageous move is undefined, or the ID number of the cell in which to put the FDI sign. If the returned value is greater than zero, select a block cell for the move, if less, select a random block cell from 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 winning move was made by FDI.
Further in main.js are less interesting things. Here we first track the situation of a draw. If the cells have ended and the winner has not been determined, then a draw. Variable done so that the determination of a tie does not occur before or after the game.
Next, the code for choosing the side by the player and the code for the “Again?” Button.
We pass in ai.js, probably to the most interesting. While it is scarce and consists of only 44 lines. But there are thoughts and ideas for him, the vacation is over and everything rests on time ...
The previously seen selectFavoriteRun function. So far, the determination of the best move for FDI is based on two principles. The first is if there is a move that will allow FDI to win, well, so do it. If there is none. Then the second principle comes into effect, if there is a danger that the player will win the next move, we are doing our best to prevent him from doing this. If such a situation is not expected, return zero. And let the random decides where to go.
So, 3-4 lines, we determine all the signs of the player on the field. We take the identifiers of these cells and pass it into another function determiningPlaceForRun, which we will consider below. We get from the function either an identifier advantageous for the course of the cell, or zero.
In lines 8 through 11, we do the same with the signs of FDI, to determine a possible winning move. Well, we return the result. If there is an option for victory, return it, no, then return the move to prevent the player from winning.
Well, the most terrible function is determining the cage for the move. The principle is the same for finding the option of victory, and to prevent the player from winning. In the loop we go through the list of winning lines. We lay the two variables points_in_row, point_for_win. The first for calculating the same type of signs in the victory lines, the second for a potentially profitable move.
After we launch the nested for, which iterates over all the already existing signs on the field. The condition on line 8 checks if there is a sign in the winning line. If there are two such characters. So there is an opportunity to win, or lose (depending on whose signs we are checking). On line 12, we assign the remaining square from the winning line, a variable for a profitable move. From the 13th to the 17th line we play it safe we check whether the cell is free. If not, cancel the profitable move. (Actually, because of the lack of optimality of logic, there is no way without this check at all. Since we check for the presence of only one kind of signs 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 zero will fly out of the function.
That is my primitive Tiktaktoy so far. In the future, there are plans to go deep into ES6 and use it to its full potential. Refine ai.js so that FDI does not use randomness during the course, but immediately seeks to win. As well as the implementation of game difficulty levels. But everything rests against time. Thank you all for your attention and good!
Link to the GitHub project.
What to write on? I decided to choose native JavaScript in order to pull up my skill in one of the most controversial programming languages. What to write? Although I’m doing web development, I have long been fond of 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 for this work, but remembering the process, I think I spent 15-20 hours playing the game. I didn’t especially follow the time, so I can be wrong. Spent an hour or two a day, not more. I tried to get the most out of 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 that in some places decisions may be far from ideals, but I am ready for dialogue, criticism, or simply 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 near-zero with me, so I ask you not to kick much for the 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 you only about two, because helpers.js has nothing particularly interesting, it describes auxiliary and trivial functions. You can watch it yourself, I am also ready for their criticism.
Another note, I did not set the goal of cross-browser compatibility, everything was checked by me on the latest Chrome. Some features of ES6 are also used, so there may be problems with browsers that are not friendly 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 similarity of AI (the language does not dare to call it just AI, so let me continue to call it FDI).
We proceed to the analysis.
A small digression on the variables. The only thing I'll cover 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 player and ai variables determine what character the player and FDI play, who has a dagger and who has a toe. The variable first_run determines who goes first. The blocks variable contains an array of html elements, the so-called cells on the playing field. Well, the win_lines variable is a two-dimensional array of winning lines, the collection of one of which is the goal of the game.
Start of 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);
}
});
});
We agree, signs I will call the crosses or zeros.
We sort through the array of elements and hang on each cell of the playing field of the listener on a click. By clicking on the cell, if there is no sign in it, put the player’s sign. On lines 9-10, we count all the signs of both 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 walk in turn. On line 19, the function puts the player’s sign in the cell by which he clicked, this procedure is described in helpers.js nothing interesting. At the end, we determine whether the player made the winning move, if so, the game is over. The functions indentifyWinner and endPlay are described there, 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 the player’s progress and conveys FDI initiative. Line 1, the variable original_points_player stores the original number of player characters. Initial - meaning, until his last move. On the 4th line we count the player’s signs, 6th line, if their number has increased, then the player has made a move. We pass on the FDI initiative. We select empty blocks (cells on the playing field) and if they are, we give the right to move FDI. The selectFavoriteRun () function, into which we pass empty cells, is described in ai.js, which we will consider 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 advantageous move is undefined, or the ID number of the cell in which to put the FDI sign. If the returned value is greater than zero, select a block cell for the move, if less, select a random block cell from 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 winning move was made by 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 are less interesting things. Here we first track the situation of a draw. If the cells have ended and the winner has not been determined, then a draw. Variable done so that the determination of a tie does not occur before or after the game.
Next, the code for choosing the side by the player and the code for the “Again?” Button.
We pass in ai.js, probably to the most interesting. While it is scarce and consists of only 44 lines. But there are thoughts and ideas for him, the vacation is over and everything rests on time ...
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;
};
The previously seen selectFavoriteRun function. So far, the determination of the best move for FDI is based on two principles. The first is if there is a move that will allow FDI to win, well, so do it. If there is none. Then the second principle comes into effect, if there is a danger that the player will win the next move, we are doing our best to prevent him from doing this. If such a situation is not expected, return zero. And let the random decides where to go.
So, 3-4 lines, we determine all the signs of the player on the field. We take the identifiers of these cells and pass it into another function determiningPlaceForRun, which we will consider below. We get from the function either an identifier advantageous for the course of the cell, or zero.
In lines 8 through 11, we do the same with the signs of FDI, to determine a possible winning move. Well, we return the result. If there is an option for victory, return it, no, then return the move to prevent the player from winning.
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 determining the cage for the move. The principle is the same for finding the option of victory, and to prevent the player from winning. In the loop we go through the list of winning lines. We lay the two variables points_in_row, point_for_win. The first for calculating the same type of signs in the victory lines, the second for a potentially profitable move.
After we launch the nested for, which iterates over all the already existing signs on the field. The condition on line 8 checks if there is a sign in the winning line. If there are two such characters. So there is an opportunity to win, or lose (depending on whose signs we are checking). On line 12, we assign the remaining square from the winning line, a variable for a profitable move. From the 13th to the 17th line we play it safe we check whether the cell is free. If not, cancel the profitable move. (Actually, because of the lack of optimality of logic, there is no way without this check at all. Since we check for the presence of only one kind of signs 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 zero will fly out of the function.
That is my primitive Tiktaktoy so far. In the future, there are plans to go deep into ES6 and use it to its full potential. Refine ai.js so that FDI does not use randomness during the course, but immediately seeks to win. As well as the implementation of game difficulty levels. But everything rests against time. Thank you all for your attention and good!
Link to the GitHub project.