Dans un premier temps, Quoridor en ligne était programmé en php, mais le serveur n'appréciait pas trop les grosses boucles de l'IA. J'ai donc réécris Quoridor en Javascript, ce qui a multiplié au moins par 10 les porformances du jeu.
Regardons d'abord comment est modélisé le plateau du jeu Quoridor, puis comment on vérifie qu'un joueur n'est pas bloqué et enfin quel est l'algorithme de l'IA.
La modélisation du Plateau de Jeu
Pour modéliser le plateau de jeu, j'ai utilisé un graphe non orienté.
Chaque case est représentée par un noeud du graphe et chaque déplacement possible par un arc de valeur 1
Ce graphe est représenté par sa matrice adjacente. Les 81 noeuds (9 x 9) sont placés verticalement et horizontalement
On a donc une matrice avec 81 lignes et 81 colonnes. Pour des raisons de simplicité, j'ai créé un tableau 100x100
Je n'utilise les lignes et les colonnes qu'à partir de la deuxième ligne, La première case du plateau est donc la case 11 (voir schéma)
Ainsi, le premier chiffre représente la ligne et le second la colonne.
Exemple de la case 25 : deuxième ligne, cinquième colonne.
Ainsi si un déplacement est possible la valeur 1 est placée dans le tableau sinon ce sera la valeur 0. Cette matrice représentera donc la matrice adjacente
Par exemple si on peut passer de la case 25 à la case 26, alors à la ligne 25 et à la colonne 26 il y a un 1.
Concrètement, la matrice adjacente en javascript correspond à une liste (ou un tableau) de 100 éléments, dont chaque élément est une liste de 100 éléments.
Ainsi, si : Graphe[25][26] = 1 // On peut passer de la case 25 à 26.
Et si Graphe[25][27] = 0 // On ne peut pas passe de la case 25 à 27.
De même si Graphe[25][35] = 1 // On peut passer de la case 25 à 35 en dessous.
Et Graphe[25][37] = 0 // On ne peut pas passe de la case 25 à 37.
Lorsqu'on place un mur, on change la valeur du graphe de 1 en 0 à 4 endroits, car les murs séparent 2 cases et le passage se fait dans les deux sens
Le parcours du graphe en largeur
Une des règles de Quoridor est l'interdiction de bloquer un joueur.
Pour vérifier qu'un joueur peut toujours accéder à une case de la ligne d'arrivée, il est nécessaire de parcourir le graphe.
Il existe deux types de parcours, le parcours (ou descente) en profondeur DFS, ou le parcours (ou descente) en largeur BFS
Pour ce jeu j'ai choisi de faire un parcours en largeur. Certains me rétorquerons qu'une descente en profondur serait plus rapide, mais c'est mon choix.
Le parcours en largeur utilise une File foncionnant sur le principe FIFO (First In First Out ou Premier entré premier sorti).
Pour décrire rapidement l'algorithme :
- le premier noeud est mis dans la File et on cherche les noeuds accessibles.
- Ces noeuds sont mis dans la File et le premier noeud est stocké dans une liste pour être marqué comme visité
- Le premier noeud est sorti de la File et on s'intéresse à ses successeurs, et ainsi de suite jusqu'à ce que la File soit vide.
Voici une implémentation en JavaScript du parcours en largeur du graphe :
//***************************************************************************************** // Parcours ou descente en largeur dans le graphe (matrice adjacente) // //***************************************************************************************** // initialisation game.graphe.init = function (graphe) { var visited = [] ; for(var i = 0; i < graphe.length ; i++){ visited[i] = 0; //initialise la liste des noeuds visités à 0 (i est le noeud) } return visited ; } ; // méthode de descente en largeur game.graphe.descenteEnLargeur = function(graphe, start, visited,ligneDestination){ var valide = false ; // creation d'une file vide var file = []; // intialisation de la file avec le premier sommet file.push(start); visited[start] = 1; var t ; var arc ; var lignedescente ; while (file.length != 0) { t = file.splice(0,1) ; // sort le premier élément de la file for(var i =0 ; i < graphe[t].length ; i++) { arc = graphe[t][i] ; if (visited[i] == 0 && arc == 1) { visited[i] = 1; file.push(i) ; // Partie spécifique au jeu pour vérifier si une ligne est atteinte lignedescente = Math.floor(i/10) ; if (lignedescente == ligneDestination) { valide = true ; } // Fin de la partie spécifique à ce jeu } } } return valide ; // return visited ; //de façon + générale } ;L'Heuristique de l'IA de Quoridor
L'idée est simple, l'IA du jeu détermine le plus court chemin des deux pions et leurs longueurs respectives
Soit DAVS la distance à parcourir par le joueur, l'adversaire jusqu'à la ligne finale.
Soit DIA la distance à parcourir par l'IA, jusqu'à la ligne finale.
L'algorithme compare les deux valeurs si : \[ D_{ADV} < D_{IA} \] alors le pion de l'IA avance. Sinon, le programme cherche à placer un mur sur le plus court chemin de l'adversaire.
Pour calculer les plus courts chemins, j'ai utilisé l'algorithme de Floyd-Warshall et non celui de Dijkstra.
En effet, l'algorithme de Floyd-Warshall calcule les plus courts chemins de tous les noeuds du graphe, alors que Dijstra calcule les plus courts chemins à partir d'un noeud initial.
Comparons la complexité respective de ces deux algorithmes :
Nombre de sommets : \[ 9 \times 9 = 81 \] Complexité de l'algorithme de Floyd-Warshall : \[ O(V^3) = 81^3 = 531\,441 \] Pour l'algorithme de Dijstra ou A*, la complexité vaut : \[ O(E + V \log V) = 144 + 81 \log(81) \approx 500 \] Mais il faut encore multiplier par 9 pour les 9 points de la ligne finale, soit 9 x 500 = 4500
On peut encore faire mieux grâce au "noeud du tricheur algorithmique".
Le principe de cette astuce consiste à ajouter un sommet supplémentaire derrière les deux lignes d'arrivées, non accessible par les pions mais bien par les algorithmes.
Ce noeud est relié aux 9 noeuds finaux. Ainsi, une seule utilisation de l'algorithme de Dijstra vers ce noeud caché suffit pour déterminer le plus court chemin en une seule fois.
Bon, j'ai fait le choix de Foyd-Warschall, mais cela me permettra, à l'avenir de créer une nouvelle IA. Nous n'en avons pas encore fini avec notre séquence d'IA. Si le programme ne trouve pas de murs à poser, le pion avance selon le plus court chemin.
Il existe encore quelques subtilités stratégiques, mais je les garde pour moi, ou alors il faudra me les demander.
Voici une implémentation en javascript de l'algorithme de Floyd-Warschall :
// ******************************************************************************************************** // algorithme de Floyd Warshall pour trouver les plus cours chemin dans le graphe à partir d'un sommet // //********************************************************************************************************** game.graphe.FloydWarshall =function(grapheJ){ var jeuG = grapheJ ; var INFINI = 1000 ; //var len = jeuG.length ; var len = 100; var valeur ; var chemins = []; var dist = new Array(100); for (var i = 0 ; i<=99 ; i++) { dist[i] = new Array(); } var pred = new Array(100); for (var i = 0 ; i<=99 ; i++) { pred[i] = new Array(); } // boucles d'initialisation de l'algorithme for (var i=0 ; i < len ; i++) { for (j=0 ; j < len ; j++) { pred[i][j] = j ; dist[i][j] = INFINI ; if((jeuG[i][j]) == 1){ valeur = jeuG[i][j]; dist[i][j] = valeur; } } } // boucles de détermination des plus courts chemins de l'ensemble du graph (distances et chemins) for (var k = 0; k < len; k++){ for (var i = 0; i < len; i++) { for (var j = 0; j < len; j++) { if (dist[i][j] > (dist[i][k] + dist[k][j])) { dist[i][j]= (dist[i][k] + dist[k][j]) ; pred[i][j] = pred[i][k] ; } } } } chemins[0] = dist ; chemins[1] = pred ; return chemins ; } ; //**************************************************************************************// // Recherche des chemins les plus courts à partir des sommets initiaux et finaux // //**************************************************************************************// game.graphe.courtChemin = function (start, end, pred){ var chemin = []; chemin.push(start) ; suivant = pred[start][end] ; chemin.push(suivant) ; while(suivant!=end){ suivant = pred[suivant][end]; chemin.push(suivant) ; } return chemin ; }
J'espère que cette description vous aura intéressé.
Laissez vos commentaires avec vos critiques et vos conseils :