/********************************* DESCRIPTION DU JEU DU TIC-TAC-TOE *********************************/ /* Une situation est decrite par une matrice 3x3. Chaque case est soit un emplacement libre (Variable LIBRE), soit contient le symbole d'un des 2 joueurs (o ou x) Contrairement a la convention du tp precedent, pour modeliser une case libre dans une matrice on n'utilise pas une constante speciale (ex : nil, 'vide', 'libre','inoccupee' ...); On utilise plut�t un identificateur de variable, qui n'est pas unifiee (ex : X, A, ... ou _) . La situation initiale est une "matrice" 3x3 (liste de 3 listes de 3 termes chacune) o� chaque terme est une variable libre. Chaque coup d'un des 2 joureurs consiste a donner une valeur (symbole x ou o) a une case libre de la grille et non a deplacer des symboles deja presents sur la grille. Pour placer un symbole dans une grille S1, il suffit d'unifier une des variables encore libres de la matrice S1, soit en ecrivant directement Case=o ou Case=x, ou bien en accedant a cette case avec les predicats member, nth1, ... La grille S1 a change d'etat, mais on n'a pas besoin de 2 arguments representant la grille avant et apres le coup, un seul suffit. Ainsi si on joue un coup en S, S perd une variable libre, mais peut continuer a s'appeler S (on n'a pas besoin de la designer par un nouvel identificateur). */ %:- use_module(library(clpfd)). situation_initiale([ [_,_,_], [_,_,_], [_,_,_]]). % Convention (arbitraire) : c'est x qui commence joueur_initial(x). cpu_time(Goal, Elapsed_Time) :- statistics(process_cputime,Start), call(Goal), statistics(process_cputime,Finish), Elapsed_Time is (Finish-Start)*1000. % Definition de la relation adversaire/2 adversaire(x,o). adversaire(o,x). /**************************************************** DEFINIR ICI a l'aide du predicat ground/1 comment reconnaitre une situation terminale dans laquelle il n'y a aucun emplacement libre : aucun joueur ne peut continuer a jouer (quel qu'il soit). ****************************************************/ situation_terminale(_Joueur, Situation) :- ground(Situation). /*************************** DEFINITIONS D'UN ALIGNEMENT ***************************/ alignement(L, Matrix) :- ligne( L,Matrix). alignement(C, Matrix) :- colonne( C,Matrix). alignement(D, Matrix) :- diagonale(D,Matrix). /******************************************** DEFINIR ICI chaque type d'alignement maximal existant dans une matrice carree NxN. ********************************************/ ligne(L, M) :- member(L,M). colonne_aux(_,[],[]). colonne_aux(N,[X|Cr],[L|Mr]):- nth1(N,L,X), colonne_aux(N,Cr,Mr). colonne(C,M) :- colonne_aux(_,C,M). %colonne(C,M) :- transpose(M,T), member(C,T). /* Definition de la relation liant une diagonale D a la matrice M dans laquelle elle se trouve. il y en a 2 sortes de diagonales dans une matrice carree(https://fr.wikipedia.org/wiki/Diagonale) : - la premiere diagonale (principale) : (A I) - la seconde diagonale : (Z R) A . . . . . . . Z . \ . . . . . / . . . \ . . . / . . . . . \ . / . . . . . . . X . . . . . . / . \ . . . . . / . . . \ . . . / . . . . . \ . R . . . . . . . I */ diagonale(D, M) :- premiere_diag(1,D,M). % deuxieme definition A COMPLETER diagonale(D, M) :- seconde_diag(3,D,M). premiere_diag(_,[],[]). premiere_diag(K,[E|D],[Ligne|M]) :- nth1(K,Ligne,E), K1 is K+1, premiere_diag(K1,D,M). seconde_diag(_,[],[]). seconde_diag(K,[E|D],[Ligne|M]) :- nth1(K,Ligne,E), K1 is K-1, seconde_diag(K1,D,M). /***************************** DEFINITION D'UN ALIGNEMENT POSSIBLE POUR UN JOUEUR DONNE *****************************/ possible([X|L], J) :- unifiable(X,J), possible(L,J). possible( [], _). /* Attention il faut juste verifier le caractere unifiable de chaque emplacement de la liste, mais il ne faut pas realiser l'unification. */ % A FAIRE unifiable(X,J) :- ( var(X) -> true ; J=X) . /********************************** DEFINITION D'UN ALIGNEMENT GAGNANT OU PERDANT POUR UN JOUEUR DONNE J **********************************/ /* Un alignement gagnant pour J est un alignement possible pour J qui n'a aucun element encore libre. */ /* Remarque : le predicat ground(X) permet de verifier qu'un terme prolog quelconque ne contient aucune partie variable (libre). exemples : ?- ground(Var). no ?- ground([1,2]). yes ?- ground(toto(nil)). yes ?- ground( [1, toto(nil), foo(a,B,c)] ). no */ /* Un alignement perdant pour J est un alignement gagnant pour son adversaire. */ % A FAIRE alignement_gagnant(Ali, J) :- possible(Ali,J), ground(Ali). alignement_perdant(Ali, J) :- adversaire(J,A), alignement_gagnant(Ali, A). /* **************************** DEFINITION D'UN ETAT SUCCESSEUR ****************************** */ /* Il faut definir quelle operation subit la matrice M representant l'Etat courant lorsqu'un joueur J joue en coordonnees [L,C] */ % A FAIRE successeur(J, Etat,[L,C]) :- nth1(L,Etat,Lig), nth1(C,Lig,X), var(X), X=J. /************************************** EVALUATION HEURISTIQUE D'UNE SITUATION **************************************/ /* 1/ l'heuristique est +infini si la situation J est gagnante pour J 2/ l'heuristique est -infini si la situation J est perdante pour J 3/ sinon, on fait la difference entre : le nombre d'alignements possibles pour J moins le nombre d'alignements possibles pour l'adversaire de J */ heuristique(J,Situation,H) :- % cas 1 H = 10000, % grand nombre approximant +infini alignement(Alig,Situation), alignement_gagnant(Alig,J), !. heuristique(J,Situation,H) :- % cas 2 H = -10000, % grand nombre approximant -infini alignement(Alig,Situation), alignement_perdant(Alig,J), !. % on ne vient ici que si les cut precedents n'ont pas fonctionne, % c-a-d si Situation n'est ni perdante ni gagnante. % A FAIRE cas 3 heuristique(J,Situation,H) :- findall(X, (alignement(X,Situation),possible(X,J)),Lgagnant), adversaire(J,A), findall(Y, (alignement(Y,Situation),possible(Y,A)),Lperdant), length(Lgagnant, LG), length(Lperdant, LP), H is LG-LP. test_heuristique :- ( ( S1= [ [_,_,_], [_,_,_], [_,_,_] ], heuristique(x,S1,0), S2 = [ [x,o,_], [_,_,_], [_,_,_] ], heuristique(x,S2,1), S3 = [ [_,o,_], [_,x,_], [_,_,_] ], heuristique(x,S3,2), S4 = [ [o,_,_], [_,x,_], [_,_,_] ], heuristique(x,S4,1), S5 = [ [x,_,_], [_,o,_], [_,_,_] ], heuristique(o,S5,1), S6 = [ [x,_,_], [_,x,_], [_,_,x] ], heuristique(o,S6,-10000), S7 = [ [x,_,_], [_,x,_], [_,_,x] ], heuristique(x,S7,10000) ) -> write('test heuristique successful') ; write('error heuristique test') ). /******************************************* * * * * * NEGAMAX * * * * *****************************************/ /* Ce programme met en oeuvre l'algorithme Minmax (avec convention negamax) et l'illustre sur le jeu du TicTacToe (morpion 3x3) */ /*:- [tictactoe].*/ /**************************************************** ALGORITHME MINMAX avec convention NEGAMAX : negamax/5 *****************************************************/ /* negamax(+J, +Etat, +P, +Pmax, [?Coup, ?Val]) SPECIFICATIONS : retourne pour un joueur J donne, devant jouer dans une situation donnee Etat, de profondeur donnee P, le meilleur couple [Coup, Valeur] apres une analyse pouvant aller jusqu'a la profondeur Pmax. Il y a 3 cas a decrire (donc 3 clauses pour negamax/5) 1/ la profondeur maximale est atteinte : on ne peut pas developper cet Etat ; il n'y a donc pas de coup possible a jouer (Coup = rien) et l'evaluation de Etat est faite par l'heuristique. 2/ la profondeur maximale n'est pas atteinte mais J ne peut pas jouer ; au TicTacToe un joueur ne peut pas jouer quand le tableau est complet (totalement instancie) ; il n'y a pas de coup a jouer (Coup = rien) et l'evaluation de Etat est faite par l'heuristique. 3/ la profondeur maxi n'est pas atteinte et J peut encore jouer. Il faut evaluer le sous-arbre complet issu de Etat ; - on determine d'abord la liste de tous les couples [Coup_possible, Situation_suivante] via le predicat successeurs/3 (deja fourni, voir plus bas). - cette liste est passee a un predicat intermediaire : loop_negamax/5, charge d'appliquer negamax sur chaque Situation_suivante ; loop_negamax/5 retourne une liste de couples [Coup_possible, Valeur] - parmi cette liste, on garde le meilleur couple, c-a-d celui qui a la plus petite valeur (cf. predicat meilleur/2); soit [C1,V1] ce couple optimal. Le predicat meilleur/2 effectue cette selection. - finalement le couple retourne par negamax est [Coup, V2] avec : V2 is -V1 (cf. convention negamax vue en cours). A FAIRE : ECRIRE ici les clauses de negamax/5 ..................................... */ negamax(J, Etat, P, P, [nil, Val]) :- heuristique(J,Etat,Val). negamax(J,Etat, P, Pmax, [nil,Val]) :- P < Pmax , heuristique(J,Etat,Val), Val = 10000. negamax(J,Etat, P, Pmax, [nil,Val]) :- P < Pmax , heuristique(J,Etat,Val), Val = -10000. negamax(J,Etat, P, Pmax, [nil,Val]) :- P < Pmax , situation_terminale(J,Etat), heuristique(J,Etat,Val). negamax(J,Etat, P, Pmax, [Coup,Val]) :- P < Pmax , successeurs(J,Etat,Successeurs), loop_negamax(J,P,Pmax,Successeurs,Liste_Couples), meilleur(Liste_Couples,[Coup,V1]), Val is -V1. /******************************************* DEVELOPPEMENT D'UNE SITUATION NON TERMINALE successeurs/3 *******************************************/ /* successeurs(+J,+Etat, ?Succ) retourne la liste des couples [Coup, Etat_Suivant] pour un joueur donne dans une situation donnee */ successeurs(J,Etat,Succ) :- copy_term(Etat, Etat_Suiv), findall([Coup,Etat_Suiv], successeur(J,Etat_Suiv,Coup), Succ). /************************************* Boucle permettant d'appliquer negamax a chaque situation suivante : *************************************/ /* loop_negamax(+J,+P,+Pmax,+Successeurs,?Liste_Couples) retourne la liste des couples [Coup, Valeur_Situation_Suivante] a partir de la liste des couples [Coup, Situation_Suivante] */ loop_negamax(_,_, _ ,[], []). loop_negamax(J,P,Pmax,[[Coup,Suiv]|Succ],[[Coup,Vsuiv]|Reste_Couples]) :- loop_negamax(J,P,Pmax,Succ,Reste_Couples), adversaire(J,A), Pnew is P+1, negamax(A,Suiv,Pnew,Pmax, [_,Vsuiv]). /* A FAIRE : commenter chaque litteral de la 2eme clause de loop_negamax/5, en particulier la forme du terme [_,Vsuiv] dans le dernier litteral ? */ /********************************* Selection du couple qui a la plus petite valeur V *********************************/ /* meilleur(+Liste_de_Couples, ?Meilleur_Couple) SPECIFICATIONS : On suppose que chaque element de la liste est du type [C,V] - le meilleur dans une liste a un seul element est cet element - le meilleur dans une liste [X|L] avec L \= [], est obtenu en comparant X et Y,le meilleur couple de L Entre X et Y on garde celui qui a la petite valeur de V. A FAIRE : ECRIRE ici les clauses de meilleur/2 */ /*meilleur([Cm,Vm], [Cm,Vm]). meilleur([[C,V]|R], [Cm,Vm]) :- meilleur(R, [Cm,Vm]), ( Vm > V -> ).*/ meilleur([X], X). %le meilleur dans une liste a un seul element est cet element meilleur(Liste_de_Couples, MeilleurCouple ) :- Liste_de_Couples = [ [C,V] | Fin ] , meilleur(Fin,[Cout,Val]), ( Val > V -> MeilleurCouple = [C , V] ; MeilleurCouple = [Cout , Val] ). test_meilleur :- ( ( meilleur([[a,2],[b,3],[c,4] ],[a,2]), meilleur([[a,2],[b,3],[c,1] ],[c,1]), meilleur([[a,2],[b,2]],[b,2]), meilleur([[a,2]],[a,2]) ) -> write('test meilleur successful') ; write('error meilleur test') ). /****************** PROGRAMME PRINCIPAL *******************/ main(B,V, Pmax) :- situation_initiale(Ini), joueur_initial(J), negamax(J,Ini,0,Pmax,[B,V]).