Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : le TAQUIN

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 25 novembre 2012

Bonjour,

Je vous propose de réaliser un petit jeu de TAQUIN, cependant je ne vais pas créer un tutorial, dans sa définition stricte, mais un exercice, la différence est notable, puisqu'un tutorial vous prendra par la main là où un exercice vous laissera réfléchir, tout en vous donnant les bases nécessaire à la résolution du problème. Bien sur nous étudierons ensemble les différentes étapes de la création, cependant c'est à vous de vous armer du minimum requis pour réussir à suivre. Cette méthode évite de créer des tutoriaux trop longs et d'expliquer de nouveau tout le détail à chaque nouveau jeu.

Voyons avant tout à quoi cela va ressembler :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Etude préliminaire

Tout d'abord le TAQUIN c'est quoi ? (merci Wikipedia)

Il s'agit d'un jeu solitaire en forme de damier, qui découle d'une théorie mathématique. Le jeu original est composé de 15 carreaux numérotés de 1 à 15 qui glissent dans un cadre prévu pour 16 pièces (il y a donc un emplacement vide pour permettre aux 15 pièces de coulisser). Le but du jeu est de remettre dans l'ordre les 15 carreaux à partir d'une configuration quelconque.

Le principe du jeu à été ensuite étendu à de nombreuses variantes, dont le Rubik's Cube.

Pour un jeu composé de 15 pièces (c'est important pour la suite), parmi toutes les dispositions initiales, il existe 10 461 394 944 000 dispositions dont la résolution est possible et autant dont la résolution est impossible.

Il est possible de dire à l'avance si le problème posé est soluble ou non. En effet, la configuration initiale d'un taquin est une permutation de sa configuration finale. Cette permutation est dite paire si elle peut être obtenue par un nombre pair d'échanges successifs de deux cases, adjacentes ou non, vide ou non, appelés également transpositions. On montre que cette notion ne dépend pas du choix de la suite des échanges. Elle est impaire sinon. On associe également à la case vide une parité : la case vide est paire si l'on peut se rendre de la position initiale de la case vide à la position finale en un nombre pair de déplacements, impair sinon.

Le problème sera soluble si la parité de la permutation est identique à la parité de la case vide.

Bon courage……..

Mélanger les pièces du jeu aléatoirement nous expose donc à un nombre considérable de configurations insoluble, or pour un joueur il faut impérativement que le jeu soit soluble, sinon il perd tout son attrait. De là on dispose de deux solutions, soit on fait un tirage aléatoire et on demande au programme de le résoudre avant de laisser le joueur se lancer, ce qui impose de connaître les algorithmes de résolution, que je ne connais pas…. Soit on prend en compte que ce jeu a été inventé en 1870, et qu'à cette époque il n'y avait pas d'ordinateurs pour mélanger le jeu, il fallait qu'une personne déplace les pièces de manière aléatoire avant qu'un autre joueur puisse essayer de résoudre le défi. C'est une solution non mathématique, mais qui fonctionne parfaitement et qui fera un très bon exercice pour démarrer la programmation de jeux vidéo et aborder quelques notions essentielles.

Les pré-requis

On ne le répétera jamais assez, vous ne pourrez pas aborder la création de jeu vidéo si vous n'avez un minimum de connaissances en programmation, on apprend pas à faire des crêpes sans savoir ce qu'est un oeuf ;-) .

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.

Le code

Dans la série “Aie ça fait mal !”, voici tout le code d'un coup.

import flash.display.MovieClip;
import flash.geom.Rectangle;
import flash.display.Bitmap;
 
var T:int = 120;
var L:int = stage.stageHeight/T;
var C:int = stage.stageWidth/T;	
 
var startGame:StartGame = 	new StartGame();
var victoire:Victoire = 	new Victoire();	
var sonBloc:SonBloc = 		new SonBloc();	
var repere:Repere = 		new Repere();	
var chargeur:Loader = 		new Loader();	
var stockPieces:Array = 	[];
 
repere.width = T; 
repere.height = T;
 
// actions des panneaux
startGame.buttonMode = true;
victoire.buttonMode = true;
startGame.addEventListener(MouseEvent.CLICK,lancePartie); 
victoire.addEventListener(MouseEvent.CLICK,relancePartie); 
 
// chargement de l'image
chargeur.contentLoaderInfo.addEventListener(Event.COMPLETE, finCharge);
chargeur.load(new URLRequest("pieces.jpg"));
 
// découpe de l'image et création des pièces
function finCharge(e:Event):void{
 
	// boucle sur le nombre de pièces
	for (var i:int=0; i<L*C;i++){
 
		// création du contenu de la pièce
		var bitmapdata:BitmapData = new BitmapData(T,T);
		bitmapdata.copyPixels(Bitmap(chargeur.content).bitmapData, new Rectangle(i%C*T,int(i/C)*T,T,T), new Point(0,0));
		var B:Bitmap = new Bitmap(bitmapdata);
 
		var piece:MovieClip = new MovieClip();	
		stockPieces.push(piece);
		piece.place = i;
		piece.depart = false;
		piece.addChild(B);
		addChild(piece);
		if(!i) {
			piece.alpha=0;
			piece.depart=true;
		} 
	}
	placeDamier();	
	addChild(repere);
	addChild(startGame);
}
 
// lancer la partie
function lancePartie(e:MouseEvent):void{
	addEventListener(Event.ENTER_FRAME,melange); 
	removeChild(startGame);
}
 
// relancer la partie
function relancePartie(e:MouseEvent):void{
	removeChild(victoire);
	addEventListener(Event.ENTER_FRAME,melange);
}
 
// survoler une pièce
function survol(e:MouseEvent):void{
	e.target.alpha=0.5;
	repere.x = e.target.x;
	repere.y = e.target.y;
}
 
// sortie du survol
function sortie(e:MouseEvent):void{
	e.target.alpha=1;
}
 
// clic sur la pièce
function cliques(e:MouseEvent):void{
	deplace(MovieClip(e.target));
}
 
 
// affichage de la grille de pièces
function placeDamier():void{
	for (var i:int=0; i<stockPieces.length;i++){
		stockPieces[i].x= i%C*T;
		stockPieces[i].y= int(i/C)*T;
	}
}
 
// mélanger les pièces
function melange(e:Event):void{
 
	var P:MovieClip;
	var X:int;
	var Y:int;
	var E:int;
	var i:Object;
	var S:Boolean = false;
 
	for each(i in stockPieces) {
		if(!i.alpha) {
			P = MovieClip(i);
			E = stockPieces.indexOf(i);
			X = P.x/T;
			Y = P.y/T;
			break;
		}
	}
 
	for each(i in stockPieces) {
		if(i!=P && !i.depart) {
			var D:int = Math.random()*4+1;// choisi une direction
			if (D==1 && X-1>=0) deplace(stockPieces[E-1]);
			if (D==2 && X+1<=C) deplace(stockPieces[E+1]);
			if (D==3 && Y+1<=L) deplace(stockPieces[E+L]);
			if (D==4 && Y-1>=0) deplace(stockPieces[E-L]);
		}
	}
 
	repere.x = P.x;
	repere.y = P.y;
 
	for each(i in stockPieces) {
		if(!i.depart) return;
	}
 
	for each(i in stockPieces) {
		if(i.alpha) {
			i.addEventListener(MouseEvent.MOUSE_OVER, survol);
			i.addEventListener(MouseEvent.MOUSE_OUT, sortie);
			i.addEventListener(MouseEvent.CLICK, cliques);
			i.buttonMode = true;
		}
	}
	removeEventListener(Event.ENTER_FRAME,melange); 
	addEventListener(Event.ENTER_FRAME,gagne);
}
 
 
// déplacement de la pièce
function deplace(P:MovieClip):void{
	// recherche le clip invisible
	var I:int = 0;
	var V:MovieClip;
	for each(var i in stockPieces) {
		if(!i.alpha) {
			I = stockPieces.indexOf(i);
			V = MovieClip(i);
		}
	}
	// vérifie si la tuile est à coté et inverse les index
	var D:int = stockPieces.indexOf(P);
	if((int(V.x/T)==C-1 && D==I+1)) return;
	if((int(V.x/T)==0 && D==I-1)) return;
	if(D==I+1 || D==I+L || D==I-L || D==I-1){
		stockPieces[D] = stockPieces[I];
		stockPieces[I] = P;
		if (!stockPieces[I].depart) stockPieces[I].depart=true;
		placeDamier();
		sonBloc.play();
	}
}
 
 
// vérifie si le joueur a gagné
function gagne(e:Event):void{
	for each(var i in stockPieces) {
		if(stockPieces.indexOf(i)!=i.place) return;
	}
	for each(var j in stockPieces) {
		if(j.alpha) {
			j.removeEventListener(MouseEvent.MOUSE_OVER, survol);
			j.removeEventListener(MouseEvent.MOUSE_OUT, sortie);
			j.removeEventListener(MouseEvent.CLICK, cliques);
			j.buttonMode = false;
			j.depart = false;
			j.alpha = 1;
		}
	}
	repere.x = 0;
	repere.y = 0;
	addChild(victoire);
	removeEventListener(Event.ENTER_FRAME,gagne);
}

Vous avez là tout le code du jeu, ni plus, ni moins, ce n'est vraiment pas très long mais rassurez-vous, nous allons quand même étudier ça ensemble ;-)

Etude du code

Bon, les pros sont déjà en train de décortiquer pour trouver de meilleures manières de faire, laissons les chercher un peu, ils viendrons nous donner la solution dans le sujet dédié sur le forum. En attendant causons et voyons ce que tout ceci fait.

// classes utiles
import flash.display.MovieClip;
import flash.geom.Rectangle;
import flash.display.Bitmap;
 
// variables
var T:int = 120;	
var L:int = stage.stageWidth/T; 	
var C:int = stage.stageHeight/T;	
 
// objets
var startGame:StartGame = 	new StartGame();
var victoire:Victoire = 	new Victoire();		
var sonBloc:SonBloc = 		new SonBloc();	
var repere:Repere = 		new Repere();
var chargeur:Loader = 		new Loader();
var stockPieces:Array = 	[];
 
// taille du repère
repere.width = T; 
repere.height = T;

Rien de bien compliqué à ce stade, on crée les variables globales et les objets du jeu.

T = taille des pièces (largeur et hauteur)
L = le nombre de lignes de la grille
C = le nombre de colonnes de la grille

startGame = le panneau de démarrage du jeu
victoire = le panneau de fin du jeu
sonBloc = le bruitage quand on déplace une pièce
repere = un petit repère visuel qui apparaît sur la pièce survolée
chargeur = un loader qui permet de charger l'image à découper
stockPieces = un tableau de stockage où on mets toutes les pièces

Les panneaux, le repère visuel et le bruitage, sont des clips exportés pour AS placés dans la bibliothèque, c'est plus pratique que d'utiliser des ressources externes et ça réduit la taille du code pour l'exercice.

Deux astuces sont déjà présentes ici, tout d'abord le découpage en lignes et colonnes.

Le plateau de jeu est toujours un carré, donc à l'aide de la taille du plateau et celle d'une pièce on peut déterminer le nombre de lignes et de colonnes utiles pour créer une grille qui remplira toute la zone de jeu. Du coup, seule la taille des pièces est importante, le programme y adaptera la grille automatiquement.

Attention a ce que T soit toujours un multiple de la taille du plateau.

La seconde astuce concerne le repère de position, j'adapte sa taille dès le départ à la taille d'une pièce.

// actions des panneaux
startGame.buttonMode = true;
victoire.buttonMode = true;
startGame.addEventListener(MouseEvent.CLICK,lancePartie); 
victoire.addEventListener(MouseEvent.CLICK,relancePartie);

Vous êtes sensés connaître la gestion événementielle en AS3 (c'est dans les pré-requis), on ne va donc pas s'attarder dessus plus longtemps. Comme le jeu ne demande pas d'interface particulière j'ai simplement deux panneaux qui vont se superposer à la grille, celui de départ et celui de fin. Quand on clique sur un des deux panneaux on lance une action correspondante que nous détaillerons un peu plus tard.

// chargement de l'image
chargeur.contentLoaderInfo.addEventListener(Event.COMPLETE, finCharge);
chargeur.load(new URLRequest("pieces.jpg"));

On aborde une notion assez importante lorsqu'on développe des jeux vidéo. La gestion des ressources externes, et plus précisément ici de ce qui fait office de feuille de sprites. Pour le moment on charge tout simplement l'image qu'on veut découper pour jouer, et on écoute la fin du chargement.

// découpe de l'image et création des pièces
function finCharge(e:Event):void{
 
	// boucle sur le nombre de pièces
	for (var i:int=0; i<L*C;i++){
 
		// création du contenu de la pièce
		var bitmapdata:BitmapData = new BitmapData(T,T);
		bitmapdata.copyPixels(Bitmap(chargeur.content).bitmapData, new Rectangle(i%C*T,int(i/C)*T,T,T), new Point(0,0));
		var B:Bitmap = new Bitmap(bitmapdata);
 
		var piece:MovieClip = new MovieClip();
		piece.addChild(B);	
		piece.place = i;
		piece.depart = false;
		stockPieces.push(piece);
		addChild(piece);
		if(!i) {
			piece.alpha=0;
			piece.depart=true;
		} 
	}
	placeDamier();
	addChild(repere);
	addChild(startGame);
}

Je reviens deux secondes à la notion de “feuille de sprites” évoquée tout à l'heure.
Si vous savez ce que c'est sautez le paragraphe suivant.

La plupart des jeux vidéo 2D utilisent ce qu'on appelle des sprites (à ne pas confondre avec l'objet Sprite de l'AS3), ce sont des séries d'images représentant les éléments graphiques animés, ou non, du jeu. Dans Flash vous disposez d'une bibliothèque pour ranger vos ressources, mais c'est parce qu'il s'agit d'un IDE bien pratique, si vous ne disposiez que du langage vous ne pourriez pas le faire et vous seriez obligé de stocker les ressources dont vous avez besoin dans des dossiers séparés. C'est pourquoi on utilise généralement des médias externes. Les graphismes sont souvent représentés sous forme de planches à dessin regroupant toutes les étapes des mouvements des personnages (par exemple). C'est une “feuille de sprites” ou “SpriteSheet”, on la charge en mémoire et on la découpe au sein du programme. Retenez bien le procédé, car vous allez souvent l'utiliser dans vos jeux.

Techniquement comment cela se met en place ?

Une fois l'image chargée, on la découpe et on range soigneusement chaque pièce dans un tableau.

On crée une boucle qui itère sur le nombre de cases utiles, (lignes*colonnes = nombre de cases)
On crée un BitmapData (pré-requis), de la taille d'une case.
A l'aide de “copyPixel” on copie une zone précise de notre image à l'intérieur du BitmapData. L'instruction “copyPixel” utilise un rectangle pour déterminer la zone à copier, il faut repositionner ce rectangle au bon endroit sur l'image pour en copier chaque portion, donc le positionner comme si il parcourait une grille. Une petite formule va nous permettre de trouver les bonnes coordonnées du rectangle :

Position x : i%C*T
Position y : int(i/C)*T

Notez là, elle est importante, elle permet la conversion d'une liste en une grille et vice et versa.
Pour faire simple elle permet de passer de ça :

1,2,3,4,5,6,7,8,9

A ça :

1,2,3
4,5,6
7,8,9

Prenons un exemple, admettons que je soit en train de travailler la case numéro 12, que la grille de mon jeu fasse 5 colonnes et 5 lignes, et que la taille d'une pièce soit de 32 pixels :

12 limité à 5 = 2
2*32 = 64

La position “x” de la case 12 est de 64 pixels.
Prenons le même exemple pour la position “y”, mais attention il y a un piège :

12 limité à 5 = 2
2*32 = 64

Ca marche, sauf que pour chaque “x” j'aurai un “y” tout à fait identique, ça ne fait pas une grille ça, mes cases seront affichées en diagonale, moi j'ai besoin de connaître “y” non pas pour chaque case mais pour chaque ligne ;-) , c'est pourquoi cette fois je n'utilise pas le modulo, mais une division.

valeur entière de (12 divisé par 5) = 2
2*32 = 64

Ca revient au même me direz vous ?
Ok alors voyons ce que ça donne avec la case suivante, la numéro 13 :

Pour x :
13 limité à 5 = 3
3*32 = 98

Pour y :
valeur entière de (13 divisé par 5) = 2
2*32 = 64

Là ce n'est plus pareil, je conserve la ligne et j'avance dans les colonnes ;-)

Passons à la suite, je crée un clip conteneur que je remplis avec le dessin de ma case.
C'est plus pratique que de jouer avec les bitmaps pour tout un tas d'effets et de paramètres.
Chaque pièce à besoin de deux paramètres, “place” représente la position d'origine de la pièce et “depart” me sert au moment où je mélange les pièces, une pièce non mélangée à un paramètre de départ équivalent à “faux”, une fois passée à la moulinette, ce paramètre prend la valeur “vrai”, je sais ainsi si une pièce à déjà été mélangée ou pas (des précisions vont suivre).

Une fois la pièce créée et paramétrée, je l'ajoute au tableau de stockage qui regroupe toutes les pièces que je viens de créer, et je l'ajoute également à l'affichage.

Il faut une case vide pour pouvoir déplacer les autres pièces. Je prend la première pièce de la liste, celle qui représente la case haut gauche de la grille, elle est considérée comme étant déjà mélangée (vous verrez pourquoi par la suite) et elle est transparente, elle existe donc bien, mais on ne la vois pas.

Puis j'affiche toutes les pièces sous la forme d'une grille (on détaille aussi après) et j'ajoute par dessus le repère de position et le panneau de départ du jeu. Certains d'entre vous se demanderont peut être pourquoi on ne crée pas tout de suite un tableau à deux dimensions pour ranger nos cases sous la forme d'une grille. C'est avant tout une question d'optimisation et de simplification du programme, optimisation car parcourir une liste est bien plus rapide qu'un tableau à plusieurs dimensions, et simplification car on évite les boucles imbriquées à répétition.

On se débarrasse de l'interactivité ?

// lancer la partie
function lancePartie(e:MouseEvent):void{
	addEventListener(Event.ENTER_FRAME,melange); 
	removeChild(startGame);
}
 
// relancer la partie
function relancePartie(e:MouseEvent):void{
	addEventListener(Event.ENTER_FRAME,melange);
	removeChild(victoire);
}
 
// survoler une pièce
function survol(e:MouseEvent):void{
	e.target.alpha=0.5;
	repere.x = e.target.x;
	repere.y = e.target.y;
}
 
// sortie du survol
function sortie(e:MouseEvent):void{
	e.target.alpha=1;
}
 
// clic sur la pièce
function cliques(e:MouseEvent):void{
	deplace(MovieClip(e.target));
}

Vous connaissez la gestion événementielle (pré-requis).
Au survol d'une pièce le repère de position de cale dessus.
Cliquer sur la pièce lance son déplacement.

Notez que je déclare ici des fonctions qui, pour le moment, n'ont pas d'écouteurs associés, notamment pour toute l'interactivité des pièces, c'est tout à fait normal, les pièces ne seront interactives que lorsque le jeu aura réellement commencé, c'est à dire après le mélange du plateau de jeu.

Allez on mélange nos pièces, puisque c'est ce qu'il doit se passer en priorité quand on lance une partie.

// mélanger les pièces
function melange(e:Event):void{
 
	var P:MovieClip;
	var X:int;
	var Y:int;
	var E:int;
	var i:Object;
	var S:Boolean = false;
 
	for each(i in stockPieces) {
		if(!i.alpha) {
			P = MovieClip(i);
			E = stockPieces.indexOf(i);
			X = P.x/T;
			Y = P.y/T;
			break;
		}
	}
 
	for each(i in stockPieces) {
		if(i!=P && !i.depart) {
			var D:int = Math.random()*4+1;// choisi une direction
			if (D==1 && X-1>=0) deplace(stockPieces[E-1]);
			if (D==2 && X+1<=C) deplace(stockPieces[E+1]);
			if (D==3 && Y+1<=L) deplace(stockPieces[E+L]);
			if (D==4 && Y-1>=0) deplace(stockPieces[E-L]);
		}
	}
 
	repere.x = P.x;
	repere.y = P.y;
 
	for each(i in stockPieces) {
		if(!i.depart) return;
	}
 
	for each(i in stockPieces) {
		if(i.alpha) {
			i.addEventListener(MouseEvent.MOUSE_OVER, survol);
			i.addEventListener(MouseEvent.MOUSE_OUT, sortie);
			i.addEventListener(MouseEvent.CLICK, cliques);
			i.buttonMode = true;
		}
	}
	removeEventListener(Event.ENTER_FRAME,melange); 
	addEventListener(Event.ENTER_FRAME,gagne);
}

Ok, voici donc l'astuce principale de notre exercice.

Cette fonction est appelée par un événement ENTER_FRAME, elle tourne en boucle à la vitesse du projet (ips), c'est très important car le programme mélange les pièces en continu tant que toutes les pièces n'ont pas été mélangées.

On commence par déclarer les variables et objets locaux à la fonction.
Puis on fait une première boucle sur toutes les cases contenues dans le tableau de stockage.
Elle sert à trouver la case vide, on enregistre le clip de référence, son index dans le stock, et sa position dans la grille.

La seconde boucle (identique à la première) est notre grosse astuce pour avoir un jeu soluble.
Le but est de prendre la pièce vide comme référence, de tirer une direction aléatoire (haut, bas, droite ou gauche) et de vérifier si la pièce suivante qui se trouve dans cette direction peut bouger et n'a pas déjà été mélangée. Auquel cas on essayes de la déplacer.

Nous demandons donc au programme se servir de la pièce vide pour mélanger, jusqu'à ce que toutes les pièces aient été mélangées, c'est ça qui nous assure qu'il est possible de résoudre le problème car le tri n'est pas fait aléatoirement, mais en fonction d'un chemin, que le joueur peut choisir de suivre ou non, mais dans tous les cas la solution existe.

Voyons comment fonctionne cette boucle.

On commence par exclure la pièce vide de la boucle, normal c'est à partir d'elle qu'on déplace les autres, on ne la mélange pas avec elle même, et on regarde si la pièce à bouger est déjà mélangée. Si tout est ok, on tire une direction aléatoirement, puis on lance une série de tests.

Selon la direction choisie, à partir de la case vide on trouve la pièce adjacente dans la grille, elle peut être dans la case immédiatement à côté, ou dans la case située au dessus ou en dessous (donc la ligne précédente, ou à la ligne suivante). Il faut également faire attention à ce que la case testée ne sorte jamais de la grille.

Si tout est ok, on essayes de déplacer la pièce, on utilise la fonction “deplace” que nous détaillerons plus loin, à laquelle on passe la référence de la pièce.

Ca ne vous semble pas clair ?
Relisez cette partie autant de fois qu'il le faut pour que ça soit limpide, c'est sans doute la plus grosse astuce de tout le jeu, alors ne la ratez pas ;-)

Je dois également vous mettre en garde, ce procédé fonctionne quel que soit le nombre de cases que vous avez dans votre grille, mais plus ce nombre est important plus le mélange prendra du temps, car le programme doit passer sur toutes les pièces en suivant un chemin aléatoire, au delà de 200 pièces cela peut prendre plusieurs minutes pour effectuer un mélange correct. Afin d'ajouter un petit effet visuel j'ai fait en sorte que le mélange se voie à l'écran, si vous utilisez de grandes grilles pensez à ne pas afficher cette étape et à optimiser le mélange pour que le joueur n'ai pas à attendre trop longtemps avant de jouer.

Continuons, car la fonction n'est pas encore terminée, vous vous rappelez qu'elle tourne en boucle tant que toutes les cases n'ont pas été mélangées. La boucle suivante, parcours une nouvelle fois tout le stock et regarde le paramètre “depart” de toutes les cases, si jamais une des cases n'est pas encore mélangée on stoppe la fonction à cet endroit, elle repart du début à la prochaine frame.

Lorsque toutes les cases ont été triées, on relance une nouvelle boucle sur toutes les cases du stock, cela peut paraître très lourd de parcourir autant de fois ce tableau de stockage mais en fait pas tellement, puisque chaque boucle n'intervient qu'à son tour et selon certaines conditions. Je pense qu'il existe des méthodes pour optimiser tout ça mais nous n'en sommes pas encore là et je ne pense pas que l'optimisation soit un facteur essentiel pour ce type de jeu, vous dépasserez rarement les 200 cases dans un tableau, sinon le joueur mettrait des heures à réussir le puzzle ;-) Cette nouvelle boucle sert donc tout simplement à ajouter l'interactivité des pièces une fois que toutes les cases ont été mélangées, la partie peut alors commencer.

Notez que la case vide est volontairement sans aucune interactivité.

Et pour terminer la fonction, puisque maintenant toutes les cases sont mélangées et interactives, on retire l'écouteur ENTER_FRAME sur la fonction, histoire d'arrêter de mélanger du vide, et on ajoute un nouvel écouteur qui va vérifier si je joueur a gagné.

On passe ensuite au déplacement des pièces :

// déplacement de la pièce
function deplace(P:MovieClip):void{
 
	var I:int = 0;
	var V:MovieClip;
	for each(var i in stockPieces) {
		if(!i.alpha) {
			I = stockPieces.indexOf(i);
			V = MovieClip(i);
		}
	}
 
	var D:int = stockPieces.indexOf(P);
	if((int(V.x/T)==C-1 && D==I+1)) return;
	if((int(V.x/T)==0 && D==I-1)) return;
	if(D==I+1 || D==I+L || D==I-L || D==I-1){
		stockPieces[D] = stockPieces[I];
		stockPieces[I] = P;
		if (!stockPieces[I].depart) stockPieces[I].depart=true;
		placeDamier();
		sonBloc.play();
	}
}

Lorsqu'on veut déplacer une pièce, que ce soit lorsqu'on les mélange ou lorsque le joueur clique dessus, cela se passe en deux temps.

Une première boucle sur le stock repère la pièce vide, on enregistre son index dans stock, et son clip de référence.

On repère l'index de la pièce passée en paramètre.
Puis on utilise une bride que je vais détailler tout de suite.

Rappelez-vous que notre stock est une liste, et non une grille. Que se passe t'il si ma pièce vide se trouve au bord d'une colonne, par exemple à droite ? Dans une grille à deux dimensions, par construction la case adjacente de droite n'existe pas, elle est en dehors du tableau. Mais dans une liste à une dimension, la case suivante existe, elle est bien dans le tableau et elle correspond à la case qui se trouve à l'autre bout de la colonne sur la ligne inférieure de la grille. Nous sommes donc obligés de le prendre en compte et de repérer si la pièce vide est au bout d'une colonne et si le déplacement à la pièce suivante ou précédente peut se faire, sinon on interdit le mouvement de la pièce.

Lorsque la pièce peut se déplacer, on intervertis simplement les index entre la pièce vide et la pièce à déplacer au sein du stock, puis on refait une passe d'affichage pour le damier. Notez au passage que nous signalons à la pièce, si elle n'est pas déjà au courant, qu'elle a été mélangée. C'est important car c'est ici que chaque pièce apprend qu'elle est considérée comme étant mélangée, sans cette ligne le mélange ne finirait jamais.

// vérifie si le joueur a gagné
function gagne(e:Event):void{
	for each(var i in stockPieces) {
		if(stockPieces.indexOf(i)!=i.place) return;
	}
	for each(var j in stockPieces) {
		if(j.alpha) {
			j.removeEventListener(MouseEvent.MOUSE_OVER, survol);
			j.removeEventListener(MouseEvent.MOUSE_OUT, sortie);
			j.removeEventListener(MouseEvent.CLICK, cliques);
			j.buttonMode = false;
			j.depart = false;
			j.alpha = 1;
		}
	}
	repere.x = 0;
	repere.y = 0;
	addChild(victoire);
	removeEventListener(Event.ENTER_FRAME,gagne);
}

Il ne nous reste plus qu'à savoir quand le joueur à gagné, c'est à dire lorsque toutes les pièces sont dans le bon ordre. Une boucle sur le tableau de stockage vérifie si toutes les pièces sont bien à leur place, si ce n'est pas le cas on ne vas pas plus loin, sinon une seconde boucle réinitialise toutes les pièces.

Et pour terminer on replace le repère sur le clip vide et affiche le panneau de victoire.

Conclusion

Et ben voilà, on est arrivé au bout ;-)

Nous avons survolé pas mal de notions intéressantes qui vous servirons pour beaucoup de jeux : afficher une grille à partir d'une liste et assurer la conversion de l'une vers l'autre, découper une planche de sprites, jongler avec les index des tableaux, simuler un comportement, …

Ce n'est pas parfait, ce n'est pas le but d'ailleurs, et si certains d'entre vous connaissent d'autres méthodes avec des algos qui le font bien, n'hésitez pas à venir les partager sur le forum.

Merci de m'avoir lu et rendez-vous pour un prochain petit exercice.

Les sources

taquin_mb_v2.fla Format CS6