Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice 04 - TIC TAC TOE

Compatible JavaScript. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 15 octobre 2015

Bonjour,

Quatrième exercice de la série, cette fois nous allons nous attaquer au TIC TAC TOE, une fois encore un jeu très simple qui ne nécessite pas de bibliothèques particulières, juste un peu de code suffit.

Pour ceux qui nous rejoindraient à ce stade, je vous engage à lire les exercices précédents (voir pré-requis) afin de comprendre la différence que je fais entre exercice et tutorial. Pour les autres, vous connaissez le principe donc GO !.

Avant de commencer, voici à quoi tout ceci va ressembler.

*pour des raisons de sécurité il n'est pas possible de vous présenter le jeu au sein d'une page du wiki, reportez-vous à la source située en bas de cette page pour voir comment ça marche.

Etude préliminaire

Tout d'abord le TIC TAC TOE c'est quoi ? (merci Wikipedia)

Le Tic-tac-toe est un jeu de réflexion se pratiquant à deux joueurs au tour par tour et dont le but est de créer le premier un alignement. Il se joue sur une grille carrée de 3×3 cases contrairement au Morpion avec lequel il est souvent confondu et qui se joue sur des grille de 5×5 cases. Deux joueurs s'affrontent et doivent remplir chacun à leur tour une case de la grille avec le symbole qui leur est attribué. Le gagnant est celui qui arrive à aligner trois symboles identiques, horizontalement, verticalement ou en diagonale.

En raison du nombre de combinaisons limité, l'analyse complète du jeu est facile à réaliser, et c'est ce qui va nous intéresser ici. Tout comme le PONG, le code du jeu est très simple, mais il fait appel à une ébauche d'intelligence artificielle pour jouer contre un ordinateur. C'est le point important que nous allons développer aujourd'hui.

Je n'ai pas pensé à le dire lors des précédents exercices, mais le but du jeu est qu'à partir de l'étude préliminaire vous essayiez vous même de résoudre le problème avant de lire l'exercice. J'ai en effet utilisé ma propre méthode, mais votre approche de la chose est tout aussi importante que la lecture de l'exercice, comparer votre résultat et le mien vous donnera bien plus d'informations que de lire simplement la solution, de plus vous pourriez trouver une autre approche sans doute plus light et optimisée et donc me corriger dans la foulée ;-)

Les pré-requis

Pour ce programme vous devez connaître :

Si vous souhaitez plus de précisions sur ces points, je vous encourage à parcourir le Wiki de Mediabox où vous trouverez de nombreux tutoriaux détaillés.

La structure

Le support principal est une page HTML classique utilisant une simple balise canvas et intégrant une feuille de style et le script du jeu.

<!DOCTYPE html>
<html>
    <head>
        <title>Tic tac toe</title>
	<link rel="stylesheet" type="text/css" href="css/styles.css" />
	<script type="text/javascript" src="js/jeu.js"></script>
	<!--[if lt IE 9]><script type="text/javascript" src="excanvas.compiled.js"></script><![endif]-->
    </head>
    <body>
         <canvas id="canvas">Votre navigateur ne supporte pas HTML5.</canvas>
    </body>
</html>

L'habillage

J'ajoute une bordure à la balise canvas :

canvas {
    border: 1px solid black;
}

Les images

Je prépare tous mes assets, c'est à dire toutes les images prédécoupées qui vont servir à mon jeu.

Je range le tout dans le dossier “assets”, rien de compliqué à ce stade, trois images sont utilisées dans le jeu.

Le code Javascript

Allez c'est parti…

// variables
var canvas;
var ctx;
 
// variables
var W = 480;
var H = 480;
var J = 1;
var T = 160;
var C = 3;
var P = 0;
 
// tableaux
var stock = [];
var valid = [];
var libre = [];
 
// charger les images du jeu
var grille = new Image();
var tileO = new Image();
var tileX = new Image();
 
grille.src = "assets/grille.jpg";
tileO.src = "assets/TileO.jpg";
tileX.src = "assets/TileX.jpg";
 
 
window.onload = function() {
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
	canvas.width = W;
	canvas.height = H;
	init();
}
 
// initialisation du jeu
function init() {
	J = 1;
	P = 0;
	stock = [0,0,0,0,0,0,0,0,0];
	valid = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
	libre = [0,1,2,3,4,5,6,7,8];
	render();
	canvas.addEventListener("click", remplir, false);
}
 
// quel joueur joue
function remplir(e){
	if(J==1) {
		var index = parseInt(e.clientX/T)+parseInt(e.clientY/T)*C;
		if(stock[index]==0) {
			stock[index] = 1;
			libre.splice(libre.indexOf(index),1);
			render();
			checkGagne();
			setTimeout(tourOrdi, 500);
			return;
		}
	}
	if(J==2) {
		stock[P] = 2;
		libre.splice(libre.indexOf(P),1);
		render();
		checkGagne();
	}
}
 
// l'ordi choisi une case
function tourOrdi(e){
	var tab = stock.concat();
	var i;
	for (i in libre) if (verifieCase(tab,2,libre[i])) return;
	for (i in libre) if (verifieCase(tab,1,libre[i])) return;
	remplirOrdi(libre[parseInt(Math.random()*libre.length)]);
}
 
// l'ordi vérifie si la case est gagnante
function verifieCase(tab, id, b){
	tab[b] = id;
	for (var c=0; c<valid.length; c++){
		var w = true;
		for (var i=0; i<valid[c].length; i++) w = w && tab[valid[c][i]] == id;
		if (w){
			remplirOrdi(b);
			return true;
		}
	}
	tab[b] = 0;
	return false;
}
 
// l'ordi joue son coup
function remplirOrdi(p){
	P = p;
	remplir(null);
}
 
// vérifie si un joueur gagne
function checkGagne(){
	for(var c=0; c< valid.length; c++){
		var w = true;
		for (var i=0; i<valid[c].length; i++) w = w && stock[valid[c][i]] == J;
		if (w){
			if(J==1) alert("Vous gagnez, cliquez pour rejouer.");
			if(J==2) alert("L'ordinateur gagne, cliquez pour rejouer.");
			init();
			return;
		}
	}
	if (libre.length==0) {
		alert("Egalité, cliquez pour rejouer.");
		init();
		return;
	}
	J==1 ? J=2 : J=1;
}
 
// Dessine le jeu
function render() {	
	ctx.drawImage(grille,0,0);
	for(var i=0; i<stock.length; i++){
		if(stock[i]==2) ctx.drawImage(tileX, i%C*T, parseInt(i/C)*T);
		if(stock[i]==1) ctx.drawImage(tileO, i%C*T, parseInt(i/C)*T);
	}
}

Etude du programme

Allez c'est parti pour une étude rapide du programme, vous êtes sensés avoir fait les deux exercices précédents, à savoir PONG et TAQUIN, il ne va donc pas y avoir grand chose à dire sur cet exercice précis.

// variables
var canvas;
var ctx;
 
// variables
var W = 480;
var H = 480;
var J = 1;
var T = 160;
var C = 3;
var P = 0;
 
// tableaux
var stock = [];
var valid = [];
var libre = [];
 
// charger les images du jeu
var grille = new Image();
var tileO = new Image();
var tileX = new Image();
 
grille.src = "assets/grille.jpg";
tileO.src = "assets/TileO.jpg";
tileX.src = "assets/TileX.jpg";

La listes des variables globales utiles pour le code, celle des tableaux, et les variables que je considère comme des paramètres qui vont permettre d'adapter le jeu à volonté.
On retrouve le nombre de colonnes, la taille d'une case, la largeur et la hauteur de la zone de jeu, c'est toujours la même chose depuis le début des exercices. Cette fois on a que trois petites images pour tout le jeu, on va donc les charger tout de suite sans utiliser de loader particulier.

Quand la page est chargée…

window.onload = function() {
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    canvas.width = W;
    canvas.height = H;
    init();
}

On prépare le jeu.
On récupère le canvas et son contexte (la zone de dessin et les outils pour dessiner).
On fixe la taille de la zone de dessine et on lance l'initialisation du jeu.

// initialisation du jeu
function init() {
	J = 1;
	stock = [0,0,0,0,0,0,0,0,0];
	valid = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
	libre = [0,1,2,3,4,5,6,7,8];
	render();
	canvas.addEventListener("click", remplir, false);
}

C'est toujours la même construction, vous ne devriez donc pas être perdus si vous avez fait les exercices précédents.
On détermine qui est le premier joueur, et on remplis nos tableaux, ceci est une nouveauté, en fait on va utiliser trois tableaux. Le stock enregistre les coups joués et par qui, valid préenregistre les combinaisons gagnantes et libre permet de déterminer les cases restantes disponibles. On termine par le rendu graphique (pour afficher le jeu la première fois) et on ajoute un écouteur d'événement sur le canvas, comme on l'as fait pour le TAQUIN.

// quel joueur joue
function remplir(e){
	if(J==1) {
		var index = parseInt(e.clientX/T)+parseInt(e.clientY/T)*C;
		if(stock[index]==0) {
			stock[index] = 1;
			libre.splice(libre.indexOf(index),1);
			render();
			checkGagne();
			setTimeout(tourOrdi, 500);
			return;
		}
	}
	if(J==2) {
		stock[P] = 2;
		libre.splice(libre.indexOf(P),1);
		render();
		checkGagne();
	}
}

Lorsqu'on clique sur le Canvas (ou que l'ordinateur joue), il faut vérifier quel est le joueur en cours. Pour le joueur (humain), on repère la case cliquée, on vérifie que l'emplacement n'a pas déjà été joué par un des joueurs, on retire cet emplacement du tableau des cases libres, on lance le rendu pour afficher le sprite correspondant, on vérifie si le joueur à gagné et on ajoute un petit timer qui permet au jeu de laisser une latence avant que l'ordinateur ne puisse jouer, ici l'ordi joue avec un décalage d'une demi seconde, suffisant pour que le joueur (humain) n'ai pas l'impression que l'ordi répond instantanément. Pour l'ordi c'est encore plus simple, on renseigne le stock en fonction de la case choisie (voir plus bas), on met à jour les emplacements libres, on lance le rendu et on vérifie si l'ordi à gagné.

// l'ordi choisi une case
function tourOrdi(e){
	var tab = stock.concat();
	var i;
	for (i in libre) if (verifieCase(tab,2,libre[i])) return;
	for (i in libre) if (verifieCase(tab,1,libre[i])) return;
	remplirOrdi(libre[parseInt(Math.random()*libre.length)]);
}

Vous vous demandez sans doute comment l'ordi choisi la case qu'il va jouer, son “intelligence artificielle” en somme.

Lorsque c'est le tour de l'ordi, le programme commence à faire une copie des coups déjà joués, puis il va essayer de trouver une case gagnante. Pour cela il vérifie les différentes combinaisons préenregistrées et les compares aux cases disponibles (voir ci-dessous). Si aucune case n'est gagnante, il en choisi une au hasard parmi les cases libres.

// l'ordi vérifie si la case est gagnante
function verifieCase(tab, id, b){
	tab[b] = id;
	for (var c=0; c<valid.length; c++){
		var w = true;
		for (var i=0; i<valid[c].length; i++) w = w && tab[valid[c][i]] == id;
		if (w){
			remplirOrdi(b);
			return true;
		}
	}
	tab[b] = 0;
	return false;
}

Pour vérifier si une case est gagnante, le programme récupère la case a tester et sa correspondance dans le stock. Puis il fait une boucle sur toutes les combinaisons qu'il vérifie une à une avec la case en choisie, si une combinaison est validée l'ordi joue cette case, sinon il jouera une case aléatoirement (voir ci-dessus).

// l'ordi joue son coup
function remplirOrdi(p){
	P = p;
	remplir(null);
}

Quand l'ordi a finalement choisi une case à jouer, il joue son coup comme le fait l'humain.

// vérifie si un joueur gagne
function checkGagne(){
	for(var c=0; c< valid.length; c++){
		var w = true;
		for (var i=0; i<valid[c].length; i++) w = w && stock[valid[c][i]] == J;
		if (w){
			if(J==1) alert("Vous gagnez, cliquez pour rejouer.");
			if(J==2) alert("L'ordinateur gagne, cliquez pour rejouer.");
			init();
			return;
		}
	}
	if (libre.length==0) {
		alert("Egalité, cliquez pour rejouer.");
		init();
		return;
	}
	J==1 ? J=2 : J=1;
}

Reste à déterminer quand un joueur a gagné la partie.

C'est sommairement la même chose que l'IA de l'ordi, on parcoure les combinaisons, si une combinaison est valable on regarde quel joueur vient de jouer et on indique qu'il a gagné. Si aucune combinaison n'est gagnante et qu'il n'y a plus de coups à jouer on indique qu'il y a égalité. Et si aucune des conditions n'est remplie on change de joueur avant de passer au coup suivant.

// Dessine le jeu
function render() {	
	ctx.drawImage(grille,0,0);
	for(var i=0; i<stock.length; i++){
		if(stock[i]==2) ctx.drawImage(tileX, i%C*T, parseInt(i/C)*T);
		if(stock[i]==1) ctx.drawImage(tileO, i%C*T, parseInt(i/C)*T);
	}
}

On termine avec le maintenant classique rendu graphique du jeu. Il suffit cette fois de dessiner le bon sprite en fonction du joueur.

Conclusion

Rien de bien terrible dans cet exercice, il met plus en avant la logique et les calculs que l'aspect graphique et c'est pour ça que pour le moment il n'est pas justifié d'utiliser une librairie ou un framework pour nous aider dans cette tâche, mais les choses vont bientôt changer avec les prochains exercices qui vont commencer à se compliquer un peu.

Les sources