Apprendre et créer
AccueilProgrammationCréer une API PHP et javascriptJeu de Morpion / tic-tac-toe (javascript + PHP) avec les optimisations

Jeu de Morpion / tic-tac-toe (javascript + PHP) avec les optimisations

Le 17-01-2020...

Je vous ai présenté une première version du jeu du Morpion qui se joue à deux, en ligne. Le problème, c'est que le code n'était pas du tout optimisé et déclenchait un nombre de requetes trop important.

J'ai donc retravaillé le code pour obtenir une version beaucoup moins gourmande.

Pour donner un ordre d'idée, en 1 minute et une partie jouée :

  • pour la première version, on est environ à 300 requêtes et 150kb transférés, pour chaque joueur !
  • pour la deuxième version, on est à 15 requêtes et 40kb transférés...

Il n'y a pas photo !

Par contre, la première version du code est étonnamment très très dépouillée ! Alors que pour cette deuxième version il y a beaucoup plus de matière.

J'ai d'ailleurs séparé le code en de plus nombreux fichiers.

L’intérêt d'avoir écrit ce code

Ecrire un code de plus en plus complexe oblige à trouver une structure à laquelle se raccrocher. On en arrive presque à se fabriquer un framework. Et je pense que c'est une bonne chose.

On peut ainsi avancer toujours plus sans reprendre à zéro et on gagne du temps.

Le code

Bien qu'il soit peu probable que quelqu'un lise ce code un jour, je le publie comme cela. Il serait peut-être temps que j'apprenne à me faire un dépôt GitHub...

index.php

<?php
if( isset($_GET['reset']) ) {
    session_start();
    unset($_SESSION);
    session_destroy();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <title>Morpion Javascript/PHP</title>
    <link rel="stylesheet" media="screen" href="style.css">
</head>
<body>
    <div id = "mainDiv"></div>
    <script type="text/javascript" src="javascript/apiRequest.js"></script>
    <script type="text/javascript" src="javascript/javascript.js"></script>
    <script type="text/javascript" src="javascript/pagesConfig.js"></script>
    <script>
        getServerData();
    </script>
    
</body>
</html>

api.php

<?php
session_start();
$_SESSION['sessionId'] = $_SESSION['sessionId'] ?? getSessionId();
$_SESSION['page'] = $_SESSION['page'] ?? 'home';
$_SESSION['lastDataSent'] = $_SESSION['lastDataSent'] ?? '';
session_write_close();
function getSessionId() {
    $content = file_get_contents('data/sessions.txt');
    $sessions = json_decode($content, TRUE);
    $sessionId = $sessions['nextSessionId'];
    $sessions['nextSessionId']++;
    $content = json_encode($sessions);
    file_put_contents('data/sessions.txt', $content);
    return $sessionId;
}
if( isset($_GET['getServerData']) ) {
    $data = getServerData();
    if( $_GET['getServerData'] == 0 ) {
        $startMicrotime = microtime(TRUE);
        while( $data == $_SESSION['lastDataSent'] ) {
            usleep(40000);
            $data = getServerData();
            if( (microtime(TRUE) - $startMicrotime) > 60 ) break;
        }
        if( $data == $_SESSION['lastDataSent'] ) exit;
    } 
    
    $dataTosend = ($_GET['getServerData'] == 1) ? $data : minimize($data);
    
    session_start();
    $_SESSION['lastDataSent'] = $data;
    session_write_close();
    echo $dataTosend != '' ? json_encode($dataTosend) : '';
}
function getServerData() {
    session_start();
    session_write_close();
    if( $_SESSION['page'] == 'home' ) {
        $content = file_get_contents('data/gamesList.txt');
        $gamesList = json_decode($content, TRUE);
        foreach( $gamesList['gamesList'] as $key => $value ) {
            if( time() - $gamesList['gamesList'][$key]['time'] > 60*60 ) {
                unset($gamesList['gamesList'][$key]);
                $updated = 1;
            } else {
                if( $value['id2'] == '' ) {
                    // 0 s'il ne faut pas afficher boutton rejoindre, 1 pour afficher
                    $serverData['gamesList'][$key] = ($value['id1'] == $_SESSION['sessionId']) ? 0 : 1;
                }
            }
        }
        if( isset($updated) ) {
            $content = json_encode($gamesList);
            file_put_contents('data/gamesList.txt', $content);
        }
    } else if( $_SESSION['page'] == 'game' ) {
        $content = file_get_contents('data/gamesList.txt');
        $gamesList = json_decode($content, TRUE);
        if( isset($gamesList['gamesList'][$_SESSION['gameId']]) ) {
            $game = $gamesList['gamesList'][$_SESSION['gameId']];
            if( $game['id2'] == '' ) {
                $serverData['message'] = 'En attente d'un adversaire...';
            } else {
                $serverData['moves'] = $game['moves'];
                $isPlayer1 = $game['id1'] == $_SESSION['sessionId'] ? 1 : 0;
                $isNextMoveForPlayer1 = count($game['moves'])%2 == 0 ? 1 : 0;
                if( $isPlayer1 == $isNextMoveForPlayer1  ) {
                    $serverData['message'] = 'A votre tour de jouer !';
                    $serverData['myTurn'] = 1;
                } else {
                    $serverData['message'] = 'Au tour de l'adversaire.';
                    $serverData['myTurn'] = 0;
                }
                if( count($game['moves']) == 9 ) {
                    $serverData['message'] = "Match nul !";
                }
    
                $winner = winner($game['moves']);
    
                if( $winner != '' ) {
                    if( ($winner == 1 AND $isPlayer1) OR ($winner == 2 AND !$isPlayer1) ) {
                        $serverData['myTurn'] = 0;
                        $serverData['message'] = 'Gagné !';
                    } else {
                        $serverData['myTurn'] = 0;
                        $serverData['message'] = 'Perdu...';
                    }
                }
            }
        } else {
            $serverData['message'] = 'L'adversaire à quitté la partie...';
        }
    }
    
    $serverData['page'] = $_SESSION['page'];
    return $serverData;
}
function minimize($dataToSend) {
    $lastDataSent = $_SESSION['lastDataSent'];
    foreach( $dataToSend as $key => $value ) {
        if( !isset($lastDataSent[$key]) OR $lastDataSent[$key] != $value ) {
            $miniData[$key] = $value;
        }
    }
    return $miniData ?? '';
}
if( isset($_GET['newGame']) ) {
    session_start();
    $content = file_get_contents('data/gamesList.txt');
    $gamesList = json_decode($content, TRUE);
    $gameId = $gamesList['nextGameId'];
    $gamesList['nextGameId']++;
    $gamesList['gamesList'][$gameId] = [
        "time" => time(),
        "id1" => $_SESSION['sessionId'],
        "id2" => '',
        "moves" => []
    ];
    $content = json_encode($gamesList);
    file_put_contents('data/gamesList.txt', $content);
    $_SESSION['gameId'] = $gameId;
    $_SESSION['page'] = 'game';
    session_write_close();
}
if( isset($_GET['leaveGame']) ) {
    $content = file_get_contents('data/gamesList.txt');
    $gamesList = json_decode($content, TRUE);
    unset($gamesList['gamesList'][$_SESSION['gameId']]);
    $content = json_encode($gamesList);
    file_put_contents('data/gamesList.txt', $content);
    session_start();
    $_SESSION['gameId'] = null;
    $_SESSION['page'] = 'home';
    session_write_close();
}
if( isset($_GET['joinGame']) ) {
    $gameId = intval($_GET['joinGame']);
    $content = file_get_contents('data/gamesList.txt');
    $gamesList = json_decode($content, TRUE);
    $gamesList['gamesList'][$gameId]['id2'] = $_SESSION['sessionId'];
    if( rand(1, 100) < 50 ) {
        $temp = $gamesList['gamesList'][$gameId]['id1'];
        $gamesList['gamesList'][$gameId]['id1'] = $gamesList['gamesList'][$gameId]['id2'];
        $gamesList['gamesList'][$gameId]['id2'] = $temp;
    }
    $content = json_encode($gamesList);
    file_put_contents('data/gamesList.txt', $content);
    session_start();
    $_SESSION['gameId'] = $gameId;
    $_SESSION['page'] = 'game';
    session_write_close();
}
if( isset($_GET['newMove']) ) {
    $el = str_split($_GET['newMove']);
    if( $el[0] == 'c' AND $el[1] < 3 AND $el[2] < 3 ) {
        $content = file_get_contents('data/gamesList.txt');
        $gamesList = json_decode($content, TRUE);
        $game = $gamesList['gamesList'][$_SESSION['gameId']];
        $moves = $game['moves'];
        $isPlayer1 = $game['id1'] == $_SESSION['sessionId'] ? 1 : 0;
        $isNextMoveForPlayer1 = count($moves)%2 == 0 ? 1 : 0;
        if( $game['id2'] != '' AND !winner($moves) AND !in_array($_GET['newMove'], $moves) AND ($isPlayer1 == $isNextMoveForPlayer1) ) {
            $gamesList['gamesList'][$_SESSION['gameId']]['moves'][] = $_GET['newMove'];
            $content = json_encode($gamesList);
            file_put_contents('data/gamesList.txt', $content);
        }
    }
}
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;
}

apiRequest.js

let forceGetServerData = 1;
function apiRequest(action, value) {
    let xhr = new XMLHttpRequest();
    if( action == 'sendForm' ) {
        let form = document.querySelector('#' + value);
        let formData = new FormData(form);
        xhr.open('POST', 'api.php');
        xhr.send(formData);
    } else {
        actionEncoded = encodeURIComponent(action);
        valueEncoded = encodeURIComponent(value);
        parameters = value !== undefined ? actionEncoded + '=' + valueEncoded : actionEncoded;
        xhr.open('GET', 'api.php?' + parameters);
        xhr.send();
    }
    
    xhr.onload = function () {
        let response = xhr.responseText;
        divDebug = document.querySelector('#divDebug');
        if( divDebug != undefined ) {
            document.querySelector('body').removeChild(divDebug);
        }
        if( response[0] != '{' && response[0] != '[' ) {
            let divDebug = document.createElement('div');
            divDebug.id = "divDebug";
            document.querySelector('body').appendChild(divDebug);
            divDebug.innerHTML = response;
            if( response != '' ) console.log(response);
        }
        
        
        if( action == 'getServerData' ) {
            if( response != '' ) processServerData(JSON.parse(response));
            setTimeout(getServerData, 30);
        }
    };
}
function getServerData() {
    apiRequest('getServerData', forceGetServerData);
    forceGetServerData = 0;
}

javascript.js

function processServerData(data) {
    for( let key in data ) {
        if( key == 'page' ) {
            if( data[key] == 'home' ) drawPage(getPageConfigHome());
            if( data[key] == 'game' ) drawPage(getPageConfigGame());
        }
        if( key == 'gamesList' ) setTimeout(() => {UC_gamesList(data[key]) },1);
        if( key == 'message' ) setTimeout(() => {UC_message(data[key]) },1);
        if( key == 'moves' ) setTimeout(() => {UC_moves(data[key]) },1);
        if( key == 'myTurn' ) setTimeout(() => {UC_myTurn(data[key]) },1);
    }
}
function drawPage(structure) {
    let parents = structure[0];
    let elementsConfig = structure[1];
    let initialParentId = Object.keys(parents)[0];
    document.querySelector('#' + initialParentId).innerHTML = '';
    for( let parentId in parents ) {
        let childrenList = parents[parentId];
        
        for( let i=0, n=childrenList.length; i<n; i++ ) {
            let childId = childrenList[i];
            elementsConfig[childId]['parentId'] = parentId
            elementsConfig[childId].id = childId;
        }
    }
    for( let key in elementsConfig ) {
        drawElement(elementsConfig[key]);
    }
}
function drawElement(config) {
    let newElement = document.createElement(config.type);
    newElement.id = config.id;
    for( let property in config.properties ) {
        newElement[property] = config.properties[property];
    }
    document.querySelector('#' + config.parentId).appendChild(newElement);
    if( config.eventListener != undefined ) {
        newElement.addEventListener('click', config.eventListener);
    }
}
function UC_gamesList(games) {
    let divGamesList = document.querySelector('#gamesList');
    divGamesList.innerHTML = '';
    for( let gameId in games ) {
        let newLi = {
            'type' : 'li',
            'parentId' : 'gamesList',
            'id' : 'game' + gameId,
            'properties' : {'textContent' : 'Partie (' + gameId + ')'}
        }
        drawElement(newLi);
        if( games[gameId] ) {
            let newButton = {
                'type' : 'span',
                'parentId' : 'game' + gameId,
                'properties' : {'textContent' : 'Rejoindre', 'className' : 'btnJoin'},
                'eventListener' : function() {apiRequest('joinGame', gameId)}
            }
            drawElement(newButton);
        }
    }
}
function UC_message(message) {
    document.querySelector('#message').textContent = message;
}
function UC_moves(moves) {
    for( let i=0, n=moves.length; i<n; i++ ) {
        symbol = i%2 == 0 ? 'O' : 'X';
        document.getElementById(moves[i]).innerHTML = symbol;
        document.getElementById(moves[i]).classList = "case off";
    }
}
function UC_myTurn(myTurn) {
    
    var classState = myTurn ? "case on" : "case off";
    for( i=0; i<3; i++ ) {
        for( j=0; j<3; j++ ) {
            divCase = document.getElementById('c'+i+j);
            if( divCase.textContent == '' ) {
                document.getElementById('c'+i+j).classList = classState;
            }
        }
    }
}

pagesConfig.js

function getPageConfigHome() {
    let parents = [];
    let elementsConfig = [];
    parents['mainDiv'] = ['hello', 'gamesListTitle', 'gamesList', 'createGame'];
    elementsConfig.hello = {
        'type' : 'h1',
        'properties' : {'textContent' : 'Bienvenue sur Morpion Javascript !'}
    }
    elementsConfig.gamesListTitle = {
        'type' : 'div',
        'properties' : {'textContent' : 'Rejoindre une partie :'}
    }
    elementsConfig.gamesList = {
        'type' : 'ul',
        'properties' : {}
    }
    elementsConfig.createGame = {
        'type' : 'div',
        'properties' : {'textContent' : 'Créer une partie'},
        'eventListener' : function() {apiRequest('newGame')}
    }
    return [parents, elementsConfig];
}
function getPageConfigGame() {
    let parents = [];
    let elementsConfig = [];
    parents['mainDiv'] = ['gameTitle', 'message', 'game', 'leaveGame'];
    parents['game'] = ['c00','c01','c02','c10','c11','c12','c20','c21','c22']
    elementsConfig.gameTitle = {
        'type' : 'h1',
        'properties' : {'textContent' : 'Partie en cours !'}
    }
    elementsConfig.message = {
        'type' : 'div',
        'properties' : {}
    }
    elementsConfig.game = {
        'type' : 'div',
        'properties' : {}
    }
    elementsConfig.leaveGame = {
        'type' : 'div',
        'properties' : {'textContent' : 'Quitter la partie'},
        'eventListener' : function() {apiRequest('leaveGame')}
    }
    for( let i=0; i<=2; i++ ) {
        for( let j=0; j<=2; j++ ) {
            elementsConfig['c'+ i + j] = {
                'type' : 'div',
                'properties' : {'className' : 'case on'},
                'eventListener' : function() {apiRequest('newMove', 'c'+ i + j)}
            }
        }
    }
    return [parents, elementsConfig];
}

style.css

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}
  
#mainDiv {
    width: 600px;
    margin: 20px auto;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    box-shadow: 2px 2px 5px #CCC;
}
h1 {
    margin: 10px;
    font-size: 1.5em;
    font-weight: bold;
    text-align: center;
}
#createGame, #leaveGame {
    margin: 20px auto;
    padding: 10px 10px;
    color: #FFF;
    font-weight: bold;
    font-size: 1.2em;
    text-align: center;
    background-color: #2E9AFE;
    border-radius: 3px;
}
#createGame:hover, #leaveGame:hover {
    cursor: pointer;
    background-color: #045FB4;
}
li { 
    list-style-type: disc;
    margin-left: 40px;
    padding-left: 10px;
}
.btnJoin {
    display: inline-block;
    margin: 3px 10px;
    padding: 2px 10px;
    color: #FFF;
    text-align: center;
    background-color: #2E9AFE;
    border-radius: 3px;
}
.btnJoin:hover {
    cursor: pointer;
    background-color: #045FB4;
}
#message {
    margin:20px 20px;
    padding:5px 10px;
    border-radius:5px;
    background-color:#BCF5A9;
    border-color:#3ADF00;
    color:#0B610B;
}
#game {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    width: 130px;
    margin: 20px auto;
    overflow: visible;
}
.case {
    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;
}