Jeu de morpion / tic-tac-toe (javascript + PHP)
Après nos premiers pas dans l'utilisation de javascript et la manière de communiquer avec PHP pour interagir entre plusieurs utilisateurs, nous allons pouvoir passer au niveau au dessus.
Attention, le code proposé ici n'est pas du tout optimisé, on peut le voir simplement comme un test de faisabilité.
Pour un code plus optimiser, voir ici : Jeu de Morpion / tic-tac-toe (javascript + PHP) avec les optimisations
Ce que nous voulons :
- Possibilité de jouer à deux joueurs au tic-tac-toe
- Au départ, choix de créer ou rejoindre une partie
- Chaque joueur joue sur une case tour à tour
- Affichage d'un message quand la partie est terminée : Perdu, Gagné, ou Match nul
Il y a donc plusieurs difficultés à surmonter. Par exemple :
- Comment identifier les joueurs et les assigner aux parties ?
- Comment stocker les informations liées aux parties ?
Pour ces questions, j'ai retenu l'utilisation des sessions avec attribution d'un ID unique, pour l'identification et je vais utiliser une base de données pour stocker les informations de mes parties. L'utilisation ici de la base de donnée n'est peut-être pas le meilleur choix point de vue performances et utilisation de ressources... Mais il faut bien un point de départ et ça fait très bien le travail !
Tout d'abord, voici la structure de la table :
CREATE TABLE IF NOT EXISTS `games` ( `id` int(11) NOT NULL AUTO_INCREMENT, `player1` varchar(255) NOT NULL, `player2` varchar(255) DEFAULT NULL, `moves` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1; COMMIT;
Ensuite, le projet est composé de 3 fichiers : comme d'habitude il y a l'index et l'api mais j'ai également un fichier database dans lequel j'ai mis mes fonctions pour la connexion à la base de donnée et l’exécution des requêtes.
Fichier database.php :
<?php $db = newDataBase('localhost', 'morpion', 'root', ''); function newDataBase($dbHost, $dbName, $dbLogin, $dbPassword) { return new PDO('mysql:host='.$dbHost.';dbname='.$dbName.';charset=utf8', $dbLogin, $dbPassword, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)); } function myQuery($request, $parameters = []) { global $db; $req = $db->prepare($request); $req->execute($parameters); return $req; }
Fichier index.php :
<?php include('database.php'); session_start(); // Une solution non optimale que j'ai retenue pour obtenir un id unique... $_SESSION['id'] = $_SESSION['id'] ?? rand(1000000,9999999); $_SESSION['gameId'] = $_SESSION['gameId'] ?? ''; if( isset($_GET['create']) ) { $response = myQuery('SELECT id FROM games WHERE player1 = ? AND player2 IS NULL',[$_SESSION['id']])->fetch(); if( !$response ) { myQuery('INSERT INTO games (player1, moves) VALUES (?, "[]")',[$_SESSION['id']]); $response = myQuery('SELECT id FROM games ORDER BY id DESC LIMIT 0,1')->fetch(); } $_SESSION['gameId'] = $response['id']; } if( isset($_GET['exit']) ) { myQuery('DELETE FROM games WHERE id = ?',[$_SESSION['gameId']]); $_SESSION['gameId'] = null; } if( isset($_GET['join']) ) { $idGame = intval($_GET['join']); $response = myQuery('SELECT * FROM games WHERE id = ? AND player2 IS NULL',[$idGame])->fetch(); if( $response ) { $_SESSION['gameId'] = $idGame; myQuery('UPDATE games SET player2 = ? WHERE id = ?',[$_SESSION['id'], $idGame]); } } ?> <!DOCTYPE html> <html lang="fr"> <head> <title>MorpionJavascript</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <style> html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0;text-align:left} .card { width:300px; padding:10px 20px; margin:10px auto; border:1px solid black; text-align:center; overflow:visible; } p { padding:0; margin:10px 20px; } h1 { font-size:1.5em; font-weight:bold; } .case { float:left; padding:3px; margin:1px; width:40px; height:40px; border:1px solid black; text-align:center; font-size:25px; } .on:hover { cursor:pointer; background-color:#A2D246; } #message { margin:20px 20px; padding:5px 10px; border-radius:5px; background-color:#BCF5A9; border-color:#3ADF00; color:#0B610B; } </style> <script> function apiRequest(request) { var requestURL = 'API.php?' + request ; var request = new XMLHttpRequest(); request.open('GET', requestURL); request.send(); return request; } // Envoi le coup joué vers le fichier API function play(divId) { apiRequest('newMove=' + divId); } // Affiche le contenu retourné du fichier API function gameRefresh() { var request = apiRequest('gameRefresh'); request.onload = function() { response = request.responseText; if( response != '' ) { if( response[0] == '{' ) { var data = JSON.parse(response); document.getElementById('message').innerHTML = data['message']; if( data['state'] == 0 ) { var classState = "case off"; } else { var classState = "case on"; } for( i=0; i<3; i++ ) { for( j=0; j<3; j++ ) { document.getElementById('c'+i+j).classList = classState; } } for(i=0;i<data['game']['moves'].length;i++) { symbol = i%2==0?'O':'X'; document.getElementById(data['game']['moves'][i]).innerHTML = symbol; document.getElementById(data['game']['moves'][i]).classList = "case off"; } } else { //document.getElementById('message').innerHTML = 'Erreur : ' + response; } } else { //document.getElementById('message').innerHTML = 'Erreur : réponse vide'; } } } var lastHtmlContent; function gamesListRefresh() { var request = apiRequest('gamesListRefresh'); request.onload = function() { response = request.responseText; if( response != '' ) { if( response[0] == '{' || response[0] == '[' ) { var data = JSON.parse(response); var htmlContent = ''; for(i=0;i<data.length;i++) { htmlContent = htmlContent + '<p><a href = "index.php?join=' + data[i]['id'] + '">Rejoindre</a></p>'; } if( lastHtmlContent != htmlContent ) { document.getElementById('gamesList').innerHTML = htmlContent; lastHtmlContent = htmlContent; } } } } } </script> </head> <body> <?php if( !$_SESSION['gameId'] ):?> <div class = "card"> <h1>Bienvenue sur morpionJavascript !</h1> <p><a href = "index.php?create">Créer une partie</a></p> <p>Parties en attente :</p> <div id = "gamesList"></div> <script> setInterval(gamesListRefresh, 200); </script> </div> <?php else:?> <div class = "card"> <h1>Bienvenue sur morpionJavascript !</h1> <?php $response = myQuery('SELECT * FROM games WHERE id = ?',[$_SESSION['gameId']])->fetch(); ?> <p>Vous êtes joueur <?= $response['player1'] == $_SESSION['id'] ? '1 : <b>O</b>' : '2 : <b>X</b>'?></p> <p><a href = "index.php?exit">Quitter</a></p> <div id = "message"></div> <div style = "width:130px;height:130px;margin:auto;"> <?php for($i=0;$i<3;$i++) { for($j=0;$j<3;$j++) { $clear = $j==0?'clear:both;':''; echo '<div class = "case on" id = "c'.$i.$j.'" style = "'.$clear.'" onClick = "play('c'.$i.$j.'');"></div>'; } } ?> </div> </div> <script> setInterval(gameRefresh, 200); </script> <?php endif;?> </body> </html>
Fichier API.php :
<?php include('database.php'); session_start(); function winner($moves) { $winner = ''; for( $i=0; $i<3; $i++ ) { for( $j=0; $j<3; $j++ ) { if( in_array('c'.$i.$j, $moves) ) { $table[$i][$j] = array_search('c'.$i.$j, $moves)%2==0 ? 1 : 2; } else { $table[$i][$j] = ''; } } } for( $i=0; $i<3; $i++ ) { if( $table[$i][0] != '' AND $table[$i][0] == $table[$i][1] AND $table[$i][0] == $table[$i][2] ) { $winner = $table[$i][0]; } } for( $j=0; $j<3; $j++ ) { if( $table[0][$j] != '' AND $table[0][$j] == $table[1][$j] AND $table[0][$j] == $table[2][$j] ) { $winner = $table[0][$j]; } } if( $table[0][0] == $table[1][1] AND $table[0][0] == $table[2][2] ) { $winner = $table[0][0]; } if( $table[2][0] == $table[1][1] AND $table[2][0] == $table[0][2] ) { $winner = $table[2][0]; } return $winner; } if( $_SESSION['id'] ) { if( isset($_GET['gameRefresh']) ) { $game = myQuery('SELECT * FROM games WHERE id = ?',[$_SESSION['gameId']])->fetch(PDO::FETCH_ASSOC); if( $game ) { $game['moves'] = json_decode($game['moves']); $return['game'] = $game; $return['state'] = 1; $return['message'] = ''; $isPlayer1 = $game['player1'] == $_SESSION['id'] ? 1 : 0; $isNextMoveForPlayer1 = count($game['moves'])%2 == 0 ? 1 : 0; if( $isPlayer1 == $isNextMoveForPlayer1 ) { $return['message'] = 'A votre tour'; } else { $return['message'] = 'Au tour de l'adversaire'; $return['state'] = 0; } if( !$game['player2'] ) { $return['state'] = 0; $return['message'] = "En attente du joueur 2"; } if( count($game['moves']) == 9 ) { $return['state'] = 0; $return['message'] = "Match nul !"; } $winner = winner($game['moves']); if( $winner != '' ) { if( ($winner == 1 AND $isPlayer1) OR ($winner == 2 AND !$isPlayer1) ) { $return['state'] = 0; $return['message'] = 'Gagné !'; } else { $return['state'] = 0; $return['message'] = 'Perdu...'; } } } else { $return['game']['moves'] = []; $return['state'] = 0; $return['message'] = 'L'adversaire a quitté la partie...'; } echo json_encode($return); } if( isset($_GET['newMove']) ) { // Ajout du "c" de case pour éviter la suppression du 0 en début des combinaisons par conversion en INT $el = str_split($_GET['newMove']); if( $el[0] == 'c' AND $el[1] < 3 AND $el[2] < 3 ) { $response = myQuery('SELECT * FROM games WHERE id = ?',[$_SESSION['gameId']])->fetch(); $moves = json_decode($response['moves']); $isPlayer1 = $response['player1'] == $_SESSION['id'] ? 1 : 0; $isNextMoveForPlayer1 = count($moves)%2 == 0 ? 1 : 0; if( $response['player2'] AND !winner($moves) AND !in_array($_GET['newMove'], $moves) AND ($isPlayer1 == $isNextMoveForPlayer1) ) { $moves[] = $_GET['newMove']; $moves = json_encode($moves); myQuery('UPDATE games SET moves = ? WHERE id = ?',[$moves, $_SESSION['gameId']]); } } } if( isset($_GET['gamesListRefresh']) ) { $gamesList = myQuery('SELECT id FROM games WHERE player1 != ? AND player2 IS NULL',[$_SESSION['id']])->fetchAll(PDO::FETCH_ASSOC); echo json_encode($gamesList); } }
Et voilà pour cette petite application qui marque un vrai tournant dans ma pratique du javascript !
J'ai pour la première fois créé un jeu multijoueur interactif en temps réel !
Problèmes
Les problèmes de ce script sont nombreux...
- La première chose, comme pour le script de chat de l'article précédent, les requêtes ne sont pas du tout optimisées. Qu'il y ai un changement ou non, elles ont lieu toutes les 200 millisecondes et l'API renvoi tout le contenu à chaque fois...
- L'utilisation de la base de données. Pour un script comme celui là il faut s'en passer !
- L'attribution de l'ID du joueur avec un rand(), horrible !
- La manière dont je mélange le PHP et le javascript pour le rendu ne me plait pas vraiment
Nous allons donc maintenant nous pencher sur la manière d'optimiser nos scripts d'API avec rafraîchissement automatique pour limiter le nombre de requêtes et le volume de données qui transitent.
Pour aller plus loin et avoir un code optimisé : Jeu de Morpion / tic-tac-toe (javascript + PHP) avec les optimisations