Your code has a lot troublesome.
Document structure
The first problem lies in this:
<?php
include 'header.php';
?>
That then will copy and paste the header.php
whole in this place. It turns out that this header.php
contains an entire HTML, and this will be placed inside another HTML. The result will be an invalid HTML, as it will look like this:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
</body>
</html>
...
</body>
</html>
Note that you end up having two sets of tags <html>...</html>
, two <head>...</head>
and two <body>...</body>
. The simplest solution to this in my view would be to put everything into jogo.php
, eliminate the header.php
and remove unduly repeated tags.
Boolean logic
Then I see that you have problems understanding logic. For example:
function checkTurn(){
if(turn){
return true;//para x
}else {
return false;//para o
}
}
All this can be reduced to this:
function checkTurn() {
return turn;
}
Or else, reduce the places where you call the function checkTurn()
in the draw(ev)
to only use the variable turn
directly.
And I must tell you that it’s almost never right to use == true
or == false
. This practice is an addiction, do not do this. The reason is that if x
is a variable that can only be true
or false
, then x == true
is exactly the same as just x
and x == false
is exactly the same as !x
. Accordingly, the == true
is completely useless and the == false
utterly expendable.
Rewriting the function checkWin()
However, I won’t even apply the above optimization to your function checkWin()
because the best is to redo it completely. Let’s consider that it is used here:
function onAreaClick(event) {
var winner;
draw(event);
console.log(turn);
winner = checkWin();
if (winner == true) {
alert("jogador 1 ganhou");
}else if (winner==false){
alert("jogador 2 ganhou");
}
}
Note that if the function checkWin
were somehow different, we could do so:
function onAreaClick(event) {
draw(event);
console.log(turn);
if (checkWin(true)) {
alert("jogador 1 ganhou");
} else if (checkWin(false)) {
alert("jogador 2 ganhou");
}
}
And it’s based on that that I’m gonna remake it:
var sequencias = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];
function checkWin(player) {
externo:
for (var i = 0; i < sequencias.length; i++) {
var sequencia = sequencias[i];
for (var j = 0; j < sequencia.length; j++) {
if (areasArray["" + linha[j]] !== player) continue externo;
}
return true;
}
return false;
}
Note the continue externo
. This then uses a feature of the javascript language (which she borrowed from Java) that is little known. This is the continue labeled.
Also note the variable sequencias
. It is an array of arrays that defines which positions mark victories.
Reviewing the function draw(ev)
In his job event
, what is inside the if
is very similar to what is inside the else
. Therefore, it is best to try to unify them like this:
var j1 = <?php echo json_encode($_GET["j1_input"]);?>;
var j2 = <?php echo json_encode($_GET["j2_input"]);?>;
function draw(ev) {
var clickedArea = document.getElementById(event.target.id);
var elem = document.createElement("img");
elem.setAttribute("src", turn ? "x.png" : "c.png");
elem.setAttribute("height", "90");
elem.setAttribute("width", "90");
clickedArea.appendChild(elem);
areasArray[ev.target.id] = turn;
console.log("Esse espaço agora é: " + turn);
turn = !turn;
document.getElementById("vez_alert").innerHTML = "Turno de:" + turn ? j1 : j2;
}
Note that I also changed the order of some instructions to make things easier to take advantage of the variable turn
as much as possible.
Objects for each player
You use true
to represent player 1 and false
to represent player 2. This is not something very cool and gets confused. So I will define the following:
var JOGADOR_X = {
img: "x.png",
nome: "X",
input = <?php echo json_encode($_GET["j1_input"]);?>
};
var JOGADOR_O = {
img: "c.png",
nome: "O",
input = <?php echo json_encode($_GET["j2_input"]);?>
};
var VAZIO = {};
JOGADOR_X.adversario = JOGADOR_O;
JOGADOR_O.adversario = JOGADOR_X;
This then defines three different objects to represent each player and also the void. It also defines some properties that I will use in draw(ex)
further down. That way where you have it:
if (checkWin(true)) {
alert("jogador 1 ganhou");
} else if (checkWin(false)) {
alert("jogador 2 ganhou");
}
That leaves:
if (checkWin(JOGADOR_X)) {
alert("jogador X ganhou");
} else if (checkWin(JOGADOR_O)) {
alert("jogador O ganhou");
}
Or better yet:
if (checkWin(turn)) {
alert("jogador " + turn.nome + " ganhou.");
}
That from here:
var turn = true;
var areasArray = [];
var area = document.getElementsByClassName('area');
for(var i =0;i<area.length;i++){
area[i].onclick = onAreaClick;
}
Stay like this:
var turn = JOGADOR_X;
var areasJogo = {};
var areas = document.getElementsByClassName("area");
for (var i = 0; i < areas.length; i++) {
areas[i].onclick = onAreaClick;
areasJogo[areas[i].id] = VAZIO;
}
document.getElementById("vez_alert").innerHTML = "Turno de: " + turn.input;
Note that I have changed areasArray
for areasJogo
, since it is no longer an array. I have also changed area
for areas
, since it is an array. With this code the vez_alert
in javascript, we no longer need to fill it in HTML with PHP, so we can empty it:
<h2 id="vez_alert"></h2>
And I’ll combine the functions onAreaClick(ev)
and draw(ev)
into one and make the necessary changes about the players:
function onAreaClick(ev) {
console.log(turn);
var clickedArea = document.getElementById(event.target.id);
var elem = document.createElement("img");
elem.setAttribute("src", turn.img);
elem.setAttribute("height", "90");
elem.setAttribute("width", "90");
clickedArea.appendChild(elem);
areasJogo[ev.target.id] = turn;
console.log("Esse espaço agora é: " + turn.nome);
if (checkWin(turn)) {
alert("jogador " + turn.nome + " ganhou.");
}
turn = turn.adversario;
document.getElementById("vez_alert").innerHTML = "Turno de: " + turn.input;
}
And the variables j1
and j2
that I introduced earlier, can be eliminated.
The tag <center>
Finally, don’t use the tag <center>
if you want to write HTML5. This tag has been obsolete for a long time, since HTML 4 came out in 1997 to be exact. That is, this tag has been obsolete for 20 years, and it is not part of HTML5. Browsers still accept the tag <center>
only to be compatible with old pages. So use CSS instead. The CSS property text-align: center;
should be what you want:
body {
text-align: center;
}
#box {
// ...
display: inline-block;
}
Fixing bugs
Fixed these code problems, we can set off to implement new things in order to fix the following bugs:
If you click on an already filled area, it will fill it again and pass the turn. Only clicks on unfilled areas should occur.
The game does not detect the tie.
It is possible to continue playing after a player has already won.
Use alert
is somewhat annoying.
Therefore, change the function once again onAreaClick(ev)
to resolve these items:
function temEspacoVazio() {
for (var i in areasJogo) {
if (areasJogo[i] === VAZIO) return true;
}
return false;
}
function acabou() {
return checkWin(JOGADOR_X) || checkWin(JOGADOR_O) || !temEspacoVazio();
}
function onAreaClick(ev) {
if (acabou() || areasJogo[ev.target.id] !== VAZIO) return;
console.log(turn);
var clickedArea = document.getElementById(event.target.id);
var elem = document.createElement("img");
elem.setAttribute("src", turn.img);
elem.setAttribute("height", "90");
elem.setAttribute("width", "90");
clickedArea.appendChild(elem);
areasJogo[ev.target.id] = turn;
console.log("Esse espaço agora é: " + turn.nome);
var novoTexto;
if (checkWin(turn)) {
novoTexto = "Jogador " + turn.nome + " ganhou.";
} else if (!temEspacoVazio()) {
novoTexto = "Empatou. Deu velha...";
} else {
turn = turn.adversario;
novoTexto = "Turno de: " + turn.input;
}
document.getElementById("vez_alert").innerHTML = novoTexto;
}
Getting rid of area ids
We can argue that using "0"
, "1"
, "2"
, "3"
, etc. to control the ids is a bit boring. In addition, having to know that the position 0 is the upper left corner, 1 the upper side, 4 the center, 7 the lower side, etc. is also somewhat boring. However, the way the code is getting, every id of some board position is almost always taken off the board itself. If we always read the board ids before using them without them being directly in javascript, they can be anything.
There is only one place where the ids are fixed in javascript, which is in the function checkWin(player)
. We will rewrite it in a similar way to the previous one, but now using HTML classes:
var sequencias = ["linha-superior", "linha-central", "linha-inferior", "coluna-esquerda", "coluna-central", "coluna-direita", "diagonal-principal", "diagonal-secundaria"];
function checkWin(player) {
externo:
for (var i = 0; i < sequencias.length; i++) {
var sequencia = sequencias[i];
var elementos = document.getElementsByClassName(sequencia);
for (var j = 0; j < elementos.length; j++) {
if (areasJogo[elementos[j].id] !== player) continue externo;
}
return true;
}
return false;
}
And the HTML:
<div id="box">
<div id="canto-superior-esquerdo" class="area coluna-esquerda linha-superior diagonal-principal"></div>
<div id="lateral-superior" class="area coluna-central linha-superior"></div>
<div id="canto-superior-direito" class="area coluna-direita linha-superior diagonal-secundaria"></div>
<div id="lateral-esquerda" class="area coluna-esquerda linha-central"></div>
<div id="centro" class="area coluna-central linha-central diagonal-principal diagonal-secundaria"></div>
<div id="lateral-direita" class="area coluna-direita linha-central"></div>
<div id="canto-inferior-esquerdo" class="area coluna-esquerda linha-inferior diagonal-secundaria"></div>
<div id="lateral-inferior" class="area coluna-central linha-inferior"></div>
<div id="canto-inferior-direito" class="area coluna-direita linha-inferior diagonal-principal"></div>
</div>
Restarting the game
Not being able to restart the game after the game is a bit boring. To solve this, we will add a reset button:
<button id="reiniciar">Reiniciar</button>
To add this new function:
function novoJogo() {
for (var i in areasJogo) {
if (areasJogo[i] === VAZIO) continue;
var elem = document.getElementById(i);
var img = elem.childNodes[0];
elem.removeChild(img);
areasJogo[i] = VAZIO;
}
turn = JOGADOR_X;
document.getElementById("vez_alert").innerHTML = "Turno de: " + turn.input;
}
And we do it at startup:
document.getElementById("reiniciar").onclick = novoJogo;
The end result for a local game
Below your full code. You can click the blue button "Execute" down there to see it working. The only details is that:
I replaced the <?php echo json_encode($_GET["j1_input"]);?>
by "C3PO" and "R2D2" because here in Stackoverflow there is no way to run PHP in the answer, but in the background you can put any JSON you want, including through PHP.
I had to put the Urls of the full images there from your github because they are hosted on a different site from where I display them in HTML. On your website you can simply use "x.png"
and "c.png"
as you already did.
var JOGADOR_X = {
img: "https://github.com/jvpessoa10/TicTacToe/blob/master/x.png?raw=true",
nome: "X",
input: "C3PO" //<?php echo json_encode($_GET["j1_input"]);?>
};
var JOGADOR_O = {
img: "https://github.com/jvpessoa10/TicTacToe/blob/master/c.png?raw=true",
nome: "O",
input: "R2D2" // <?php echo json_encode($_GET["j2_input"]);?>
};
var VAZIO = {};
JOGADOR_X.adversario = JOGADOR_O;
JOGADOR_O.adversario = JOGADOR_X;
var turn = JOGADOR_X;
var areasJogo = {};
var areas = document.getElementsByClassName("area");
for (var i = 0; i < areas.length; i++) {
areas[i].onclick = onAreaClick;
areasJogo[areas[i].id] = VAZIO;
}
document.getElementById("vez_alert").innerHTML = "Turno de: " + turn.input;
document.getElementById("reiniciar").onclick = novoJogo;
var sequencias = ["linha-superior", "linha-central", "linha-inferior", "coluna-esquerda", "coluna-central", "coluna-direita", "diagonal-principal", "diagonal-secundaria"];
function onAreaClick(event) {
draw(event);
console.log(turn);
if (checkWin(turn)) {
alert("jogador " + turn.nome + " ganhou.");
}
}
function checkWin(player) {
externo:
for (var i = 0; i < sequencias.length; i++) {
var sequencia = sequencias[i];
var elementos = document.getElementsByClassName(sequencia);
for (var j = 0; j < elementos.length; j++) {
if (areasJogo[elementos[j].id] !== player) continue externo;
}
return true;
}
return false;
}
function temEspacoVazio() {
for (var i in areasJogo) {
console.log("vazio: " + i);
if (areasJogo[i] === VAZIO) return true;
}
return false;
}
function acabou() {
return checkWin(JOGADOR_X) || checkWin(JOGADOR_O) || !temEspacoVazio();
}
function onAreaClick(ev) {
if (acabou() || areasJogo[ev.target.id] !== VAZIO) return;
console.log(turn);
var clickedArea = document.getElementById(event.target.id);
console.log("espaço vazio");
var elem = document.createElement("img");
elem.setAttribute("src", turn.img);
elem.setAttribute("height", "90");
elem.setAttribute("width", "90");
clickedArea.appendChild(elem);
areasJogo[ev.target.id] = turn;
console.log("Esse espaço agora é: " + turn.nome);
var novoTexto;
if (checkWin(turn)) {
novoTexto = "Jogador " + turn.nome + " ganhou.";
} else if (!temEspacoVazio()) {
novoTexto = "Empatou. Deu velha...";
} else {
turn = turn.adversario;
novoTexto = "Turno de: " + turn.input;
}
document.getElementById("vez_alert").innerHTML = novoTexto;
}
function novoJogo() {
for (var i in areasJogo) {
if (areasJogo[i] === VAZIO) continue;
var elem = document.getElementById(i);
var img = elem.childNodes[0];
elem.removeChild(img);
areasJogo[i] = VAZIO;
}
turn = JOGADOR_X;
document.getElementById("vez_alert").innerHTML = "Turno de: " + turn.input;
}
body {
text-align: center;
}
#box {
width: 276px;
height: 276px;
background-color: gray;
display: inline-block;
}
#box div {
background-color: lightgray;
width: 90px;
height: 90px;
text-align: center;
float: left;
margin: 1px;
}
<!DOCTYPE html>
<html>
<head>
<title>Jogo da velha</title>
</head>
<body>
<h1>Jogo da velha</h1>
<div id="box">
<div id="canto-superior-esquerdo" class="area coluna-esquerda linha-superior diagonal-principal"></div>
<div id="lateral-superior" class="area coluna-central linha-superior"></div>
<div id="canto-superior-direito" class="area coluna-direita linha-superior diagonal-secundaria"></div>
<div id="lateral-esquerda" class="area coluna-esquerda linha-central"></div>
<div id="centro" class="area coluna-central linha-central diagonal-principal diagonal-secundaria"></div>
<div id="lateral-direita" class="area coluna-direita linha-central"></div>
<div id="canto-inferior-esquerdo" class="area coluna-esquerda linha-inferior diagonal-secundaria"></div>
<div id="lateral-inferior" class="area coluna-central linha-inferior"></div>
<div id="canto-inferior-direito" class="area coluna-direita linha-inferior diagonal-principal"></div>
</div>
<h2 id="vez_alert"></h2>
<button id="reiniciar">Reiniciar</button>
</body>
</html>
Placing the AJAX
Now that your game already works very well locally for two players sitting side by side in front of the same PC, it’s time to make the game be via internet.
First, let’s assume that on your "game.php" page, you have a request parameter called "jogador
", which may be "X
" or "O
". That is, the player accesses as jogo.php?jogador=O
or jogo.php?jogador=X
. In javascript, you put this:
var jogadores = {
"?jogador=X": JOGADOR_X,
"?jogador=O": JOGADOR_O,
};
var eu = jogadores[location.search] || VAZIO;
This will serve to identify who the player is. If the user enters without placing ?jogador=X
or ?jogador=O
in the URL, he can watch the game as a viewer. To send and receive moves via AJAX (with jQuery), create these two functions:
function enviar(localEscolhido) {
$.ajax({
url: "/fazer-jogada.php",
method: "POST",
dataType: "json"
data: {
"quem": eu.nome,
"onde": localEscolhido
}
}).fail(function(jqXHR, textStatus, errorThrown) {
// Tratar o erro...
});
}
var dicionario = {
"X": JOGADOR_X,
"O": JOGADOR_O,
" ": VAZIO
};
function atualizar() {
$.ajax({
url: "/estado-jogo.php",
cache: false,
dataType: "json"
}).done(function(json) {
for (var i in areasJogo) {
areasJogo[i] = dicionario[json[i]];
}
turn = dicionario[json.turn];
setTimeout(atualizar, 1000);
}).fail(function(jqXHR, textStatus, errorThrown) {
// Tratar o erro...
});
}
atualizar();
The function enviar
will send ids
such as canto-superior-direito
, borda-esquerda
, centro
or reiniciar
. If you want you can make it work in some different way. Already the update function keeps insisting on updating with the setTimeout
forever.
On the PHP side, you maintain an associative array with each game position (as accepted in enviar
) and also with a field turn
. Each of these fields can have the values "X"
, "O"
or " "
. The estado-jogo.php
should bring this associative array in JSON format. O fazer-jogada.php
is responsible for changing this associative array, receiving a JSON, and must check whether the field "quem"
is the player who has the turn, if the "onde"
is a blank place to play and also if the game is not over. In addition, the fazer-jogada.php
must accept the text reiniciar
within the "localEscolhido"
.
The function atualizar()
already works alone without requiring changes to the rest of the code. However, to which function enviar()
is used, some changes are necessary. So, in the function onAreaClick(ev)
, change this:
if (acabou() || areasJogo[ev.target.id] !== VAZIO) return;
That’s why:
if (acabou() || areasJogo[ev.target.id] !== VAZIO || turn !== eu) return;
And right after that:
areasJogo[ev.target.id] = turn;
Add to that:
enviar(ev.target.id);
At the end of the function novoJogo()
, add to that:
enviar("reiniciar");
Additional aspects to be considered
You can argue that anyone who enters the URL jogo.php?jogador=X
or jogo.php?jogador=O
can make the move, even if he is not the corresponding player. The solution to this is to develop a login and password functionality and send an authentication token along with JSON in the function enviar()
. All this should be validated on fazer-jogada.php
. The details of how to do this already go beyond the scope of this question, but you can find it here in Sopt itself, whether in some other question already asked or yourself asking this specific aspect.
Also there is only one game in progress on the server, and this is not something very desirable. To solve this, each game would have a name or number and that name or number would be sent along with the other data in the functions atualizar()
and enviar()
. The JSON returned by estado-jogo.php
and manipulated by fazer-jogada.php
would have to stop being global and instead be something that would be found in the database or else in an associative array in memory, using to locate it the corresponding game, the name or number of it.
Finally, the AJAX I posted above does not treat errors. To handle errors, you will add some type of code where you have comments // Tratar o erro...
. More details can be seen on jQuery AJAX documentation.
Fucking dude, you’re the same guy, thanks for practically the class. Corrected all my gambiarra even kkkkkkkkk. + 1
– jvpessoa10
@jvpessoa10 If you are satisfied with this answer and have no questions left to answer, would you please mark it as correct/accept/resolved by clicking on the side here? If you still have any questions, feel free to comment.
– Victor Stafusa