 
 
 
 Jeux à deux joueurs
L'application présentée dans cette section poursuit un double objectif. 
D'une part elle s'attaque à une résolution de problèmes 
dont la complexité empêche une exploration systématique de toutes les solutions, mettant ainsi en 
valeur la bonne adéquation du langage Objective CAML pour le traitement
d'applications symboliques. D'autre part, elle permet grâce à l'utilisation 
de modules paramétrés, de définir un cadre général pour la construction 
de jeux à deux joueurs, permettant ainsi de factoriser une partie de
la recherche et de rendre facilement modifiable des composants comme
la fonction d'évaluation d'une position d'un jeu ou son affichage. 
On présente tout d'abord la problématique des jeux à deux joueurs,
puis nous décrivons l'algorithme minimax-ab
permettant un élagage rapide de l'arbre des coups possibles. Nous
[réalisons un module paramétré implantant cet algorithme pour
décrire ensuite le module paramétré général des applications jeux
à 2 joueurs. Enfin, nous appliquons ces foncteurs pour réaliser deux
applications jeux : un puissance 4 (un morpion gravitationnel), 
et Stone Henge (un jeu de construction de lignes de force).
 Problématique des jeux à 2 joueurs
Les jeux à deux joueurs font partie des applications classiques en 
programmation symbolique. C'est un bon exemple de résolution de problèmes
pour au moins deux raisons : 
- 
 le nombre de solutions à analyser pour obtenir le meilleur coup
nécessite des méthodes autres que la force brute : aux échecs le
nombre de coups possibles est en moyenne de 30 et une partie se joue
en une quarantaine de coups pour chaque joueur; ce qui donne
quelques 3080 positions pour explorer l'arbre complet d'une partie!
-  la qualité d'une solution est facilement vérifiable, en particulier 
il est possible de tester la qualité de la solution proposée par un programme 
en utilisant un autre programme.
Supposons que l'on puisse utiliser une exploration totale de toutes les parties
possibles à partir d'une position légale du jeu. Un tel programme a besoin 
d'une fonction de génération des coups légaux à partir de cette position et
d'une fonction d'évaluation donnant un score à la position donnée. La 
fonction d'évaluation donne un score maximal à une position gagnante et un 
score minimal à une position perdante. À partir de la position initiale, 
on peut donc construire l'arbre de toutes les variantes de la partie, où chaque
noeud correspond à une position, ses fils aux positions suivantes obtenues
 en ayant joué un coup et les feuilles aux positions
gagnantes, perdantes ou nulles. Une fois cet arbre construit, son exploration
permet de déterminer s'il existe un chemin menant à la victoire, ou 
à une position nulle le cas échéant. Le chemin de plus petite
longueur peut alors être choisi pour amener au résultat voulu.
Comme la taille d'un tel arbre est en règle générale trop grande pour
être envisageable, il est nécessaire d'en limiter sa construction. La
première possibilité est de limiter la profondeur de recherche,
c'est-à-dire le nombre de coups et de réponses à évaluer. On réduit
ainsi la profondeur de l'arbre donc sa taille. Dans ce cas on atteint
rarement des feuilles à moins d'être en fin de partie.
D'autre part, nous pouvons essayer de limiter le nombre de coups
engendrés pour pouvoir les évaluer plus finement. Pour cela, nous tentons
de n'engendrer que les coups semblant les plus favorables et de les examiner
en commençant par les meilleurs. Cela permet ainsi d'élaguer
rapidement des branches entières de l'arbre. C'est justement ce que
propose l'algorithme minimax-ab 
qui est présenté dans la section suivante.
 Minimax ab
On présente la recherche minimax puis on décrit sa variante optimisée
par les coupures ab. L'implantation de cet algorithme utilise un 
module FAlphabeta paramétré par la représentation du jeu et
la fonction d'évaluation. On distingue les deux joueurs en les nommant A et B.
 Minimax
L'algorithme minimax est un algorithme de recherche en
profondeur, avec une profondeur limitée. Il nécessite d'utiliser : 
- 
 une fonction de génération des coups légaux à partir d'une position,
-  et une fonction d'évaluation d'une position de jeu. 
À partir d'une position du jeu, l'algorithme explore l'arbre de tous les coups légaux
jusqu'à la profondeur demandée. Les scores des feuilles de l'arbre sont alors calculés
par la fonction d'évaluation. 
 Un score positif indique une bonne position pour le joueur A, un score négatif une mauvaise
position pour le joueur A donc une bonne position pour le joueur B. Selon le joueur qui joue,
le passage d'une position à une autre est maximisante (pour le joueur A) ou minimisante 
(pour le joueur B). 
Les joueurs essaient de jouer les coups les plus profitables pour eux-mêmes. En 
cherchant le meilleur coup pour le joueur A, la recherche en profondeur 1
cherchera à déterminer le coup immédiat qui maximise le score de la nouvelle position.
Figure 17.1 : recherche maximisante à un niveau
Sur la figure 17.1, le joueur A part de la position O,
détermine quatre coups légaux, construit ces nouvelles configurations
et les évalue. De ces scores, sa meilleur position est P2 (de score
8). Il propage cette valeur à la position O, indiquant par là que
cette position amène en un coup à une nouvelle position de score 8 en
jouant le coup C2. L'exploration en profondeur 1 est en règle
générale insuffisante, car on ne tient pas compte de la réponse de
l'adversaire. Cela produit des programmes cherchant le gain immédiat
de matériel (comme la prise d'une reine aux échecs), sans s'apercevoir
que les pièces sont protégées ou que la position devient perdante
(gambit de la reine pour faire mat). Une exploration de profondeur 2
permet de s'apercevoir du contrecoup.
La figure 17.2 montre un niveau supplémentaire de
développement de l'arbre en tenant compte de la réponse du joueur
B. Celui-ci recherche aussi son meilleur coup. Pour cela l'algorithme
minimax minimisera les scores des noeuds de profondeur 2.
Figure 17.2 : recherche maximisante et minimisante à deux niveaux
Le coup P2 qui amenait à une position immédiate de score 8, va en fait
amener la position du jeu à un score de -3. En effet si B joue le coup
D5, alors le score de la position Q5 vaut -3. On s'aperçoit alors que
le coup C1 limite les dégats avec un score de -1. Il sera donc
préféré.
Dans la plupart des jeux, il est possible de faire lanterner son
adversaire, en le faisant jouer à coups forcés, dans le but
d'embrouiller la situation en espérant qu'il commette une faute. Pour
cela la recherche de profondeur 2 est très insuffisante pour le côté
tactique du jeu. Le côté stratégique est rarement bien exploité par un
programme car il n'a pas la vision de la probable évolution de la
position en fin de partie. La difficulté de profondeur plus grande
provient de l'explosion combinatoire. Par exemple aux échecs,
l'exploration de 2 profondeurs supplémentaires apporte un facteur
d'environ mille fois plus de combinaisons (30 * 30). Donc si on
cherche à calculer une profondeur de 10, on obtiendra environ 514
positions, ce qui est bien entendu trop. Pour cela on essaie d'élaguer
l'arbre de recherche. On peut noter que dans la figure
17.2, il n'est pas forcément utile d'explorer la branche P3 
dans la mesure où le score de cette position à la profondeur 1 (voir
figure 17.1) est déjà moins bon que celui trouvé dans la
branche P1. De même la branche P4 n'a pas besoin d'être complètement
explorée. Dès le calcul de Q7, on obtient un score inférieur à celui
de P1 (toujours complètement explorée). Les calculs de Q8 et Q9 ne
pourront pas améliorer cette situation même si leur score respectif
est meilleur que Q7. Dans une étape minimisante, le score le plus
faible est remonté. On sait déjà qu'ils n'apporteront rien de nouveau.
La variante ab du minimax utilise cet élagage pour diminuer
le nombre de branches à explorer.
 Minimax-ab
On appelle coupure a la borne inférieure d'un noeud
maximisant, et coupure b la borne supérieure d'un noeud
minimisant. La figure 17.3 montre les coupures
effectuées dans les branches P3 et P4 par la connaissance de la borne
inférieure -1 de P1.
Figure 17.3 : coupure a dans une étape max-min
Dès que l'arbre est plus large ou plus profond le nombre de
coupures augmente, élagant ainsi de grands sous-arbres.
 Module paramétré pour le minimax-ab
On désire réaliser un module paramétré FAlphabeta qui implante cet
algorithme, réutilisable pour tout jeu à deux joueurs. Ses
paramètres correspondent d'une part à toutes les informations
d'arbitrage du jeu et d'autre part à la fonction d'évaluation. 
 Interfaces
On déclare les signatures REPRESENTATION, pour la
représentation du jeu, et EVAL, pour l'évaluation d'une position.
# module type REPRESENTATION = sig
   type jeu
   type coup
   val jeu_depart : unit -> jeu
   val coups_legaux: bool -> jeu -> coup list
   val jouer: bool -> coup -> jeu -> jeu
 end;;
# module type EVAL = 
 sig
   type jeu
   val evaluer: bool -> jeu -> int
   val plusI : int
   val moinsI: int
   val est_feuille: bool -> jeu -> bool
   val est_stable: bool -> jeu -> bool
   type etat = G | P | N | C
   val etat_de : bool -> jeu -> etat
 end;;
Les types jeu et coup sont abstraits. Un joueur sera représenté par un booléen. 
La fonction coups_legaux prend un joueur, une position et retourne la liste des 
coups possibles. La fonction jouer prend un joueur, un coup, 
une position et retourne une nouvelle position. Les valeurs plusI 
et moinsI sont les bornes des valeurs retournées par la fonction evaluer. 
Le prédicat est_feuille vérifie si un joueur dans une position donnée peut jouer.
Le prédicat est_stable indique si la position pour le joueur est dans une
situation stable. Les résultats des ces fonctions influencent la poursuite de l'exploration
de coups quand on atteint la profondeur prévue.
La signature ALPHABETA correspond à la signature résultant
de l'application totale du module paramétré que l'on
désire réaliser.
Celle-ci masque les différentes fonctions auxiliaires 
que nous utiliserons pour l'implantation de l'algorithme.
# module type ALPHABETA = sig 
   type jeu
   type coup
   val alphabeta : int -> bool -> jeu -> coup
 end ;;
La fonction alphabeta prend en paramètre la profondeur de la
recherche, le joueur et la position du jeu, elle retourne le coup déterminé.
On définit enfin la signature fonctionnelle 
FALPHABETA que devra respecter l'implantation du foncteur.
# module type FALPHABETA  = 
   functor (Rep : REPRESENTATION) ->
   functor (Eval : EVAL with type jeu = Rep.jeu) ->
     ALPHABETA with type jeu = Rep.jeu and type coup = Rep.coup ;;
 Implantation
Le module paramétré FAlphabetaO explicite le partage du type jeu
entre ses deux paramètres Rep et Eval.
Ce module ne comprend que six fonctions
et deux exceptions. Le joueur true cherche à maximiser son score alors que le joueur 
false le minimise. La fonction maxmin_iter calcule le maximum entre
le meilleur score des branches 
à partir d'un coup du joueur true et la borne a. 
La fonction maxmin prend quatre paramètres : prof qui indique 
la profondeur actuelle du calcul, noeud une position du jeu, alpha et
beta les bornes des coupures. Si le noeud est une feuille de l'arbre ou si la profondeur 
maximale est atteinte alors la fonction retourne l'évaluation de la position. Si ce n'est pas le cas,
elle applique à tous les coups légaux du joueur true la fonction maxmin_iter
en lui passant la fonction de poursuite de calcul dont la profondeur est décrémentée (minmax).
Cette dernière cherchera à minimiser le score de la réponse du joueur false.
Les coupures sont implantées par des exceptions. 
Si la coupure b est déclenchée dans l'itération sur les coups légaux de la
fonction maxmin, alors celle-ci retourne immédiatement avec la valeur propagée 
par l'exception. Les fonctions minmax_iter et minmax effectuent le travail pour l'autre joueur. La fonction cherche détermine le coup à jouer à partir du meilleur score en
parcourant les listes des scores et des coups. 
La fonction principale alphabeta de ce module calcule
les coups légaux d'une position pour un joueur donné et lance les recherches à la profondeur indiquée 
puis retourne le meilleur coup.
# module FAlphabetaO 
     (Rep : REPRESENTATION)  (Eval : EVAL with type jeu = Rep.jeu)  = 
   struct
     type jeu = Rep.jeu
     type coup = Rep.coup
     exception AlphaCoupure of int 
     exception BetaCoupure of int 
  
     let maxmin_iter noeud minmax_cur beta alpha cp =
       let alpha_resu =  
         max alpha (minmax_cur (Rep.jouer true cp noeud) beta alpha) 
       in if alpha_resu >= beta then  raise (BetaCoupure alpha_resu)  
          else alpha_resu 
 
     let minmax_iter noeud maxmin_cur alpha beta cp =
       let beta_resu = 
         min beta (maxmin_cur (Rep.jouer false cp noeud) alpha beta) 
       in if beta_resu <= alpha then  raise (AlphaCoupure beta_resu)  
          else beta_resu     
 
     let rec maxmin prof noeud alpha beta =
       if    (prof < 1 & Eval.est_stable true noeud) 
          or Eval.est_feuille true noeud
       then Eval.evaluer true noeud  
       else 
         try  let prev = maxmin_iter noeud (minmax (prof - 1)) beta
              in List.fold_left prev alpha (Rep.coups_legaux true noeud)
         with BetaCoupure a -> a
 
     and minmax prof noeud beta alpha =
       if    (prof < 1 & Eval.est_stable false noeud) 
          or Eval.est_feuille false noeud
       then Eval.evaluer false noeud 
       else 
         try  let prev = minmax_iter noeud (maxmin (prof - 1)) alpha
              in List.fold_left prev beta (Rep.coups_legaux false noeud)
         with AlphaCoupure b -> b
 
     let rec cherche a l1 l2 = match (l1,l2) with 
         (h1::q1, h2::q2) -> if a = h1 then h2  else cherche a q1 q2
       | ([], []) -> failwith  ("AB: "^(string_of_int a)^" non trouve'")
       | (_ ,  _) -> failwith  "AB: longueur diff" 
 
     (* val alphabeta : int -> bool -> Rep.jeu -> Rep.coup *)
     let alphabeta prof joueur  racine = 
       let alpha = ref Eval.moinsI and beta = ref Eval.plusI in
       let l = ref [] in
       let cpl = Rep.coups_legaux joueur  racine in
       let eval =
         try 
           for i = 0 to (List.length cpl) - 1 do
             if joueur then 
               let b = Rep.jouer joueur (List.nth cpl i) racine in
               let a = minmax (prof-1) b !beta !alpha 
               in l := a :: !l ; 
                  alpha := max !alpha a ;
                  (if !alpha >= !beta then raise (BetaCoupure !alpha) )
             else
               let a = Rep.jouer joueur (List.nth cpl i) racine in
               let b = maxmin (prof-1) a !alpha !beta 
               in l := b :: !l ;
                  beta := min !beta b ;
                  (if !beta <= !alpha then raise (AlphaCoupure !beta))
             done ;
           if joueur then !alpha else !beta
         with 
               BetaCoupure a -> a
            |  AlphaCoupure b -> b
       in    
          l := List.rev !l ;
          cherche eval !l cpl
   end ;;
On peut alors fermer le module FAlphabetaO en lui associant
cette signature. 
# module FAlphabeta = (FAlphabetaO : FALPHABETA);;
module FAlphabeta : FALPHABETA
C'est ce dernier module qui est ensuite utilisé sur différentes
représentations et fonctions d'évaluation de jeu.
 Organisation d'un programme de jeux
L'organisation d'un programme de jeu à 2 joueurs doit bien séparer
la partie spécifique à un jeu donné de la partie commune à toutes les
applications de jeux. Pour cela on se propose d'utiliser plusieurs
modules paramétrés par les modules spécifiques, permettant ainsi
d'éviter de réécrire à chaque fois les parties communes. 
La figure 17.4 montre l'organisation choisie.
Figure 17.4 : organisation d'une application de jeux
Les modules sur fond blanc correspondent aux parties communes de
l'application. Ce sont des modules paramétrés. On y retrouve le
foncteur FAlphabeta décrit précédemment. Les modules sur fond
grisé sont les modules spécifiques au jeu. Les trois principaux sont
la représentation du jeu (J_Repr), l'affichage du jeu
(J_Aff) et la fonction d'évaluation (J_Eval). Les
modules aux bords gras arrondis sont obtenus par l'application des modules
paramétrés sur les modules simples spécifiques au jeu.
Le module FAlphabeta vient d'être décrit. Les deux autres
modules communs sont FMain qui contient la boucle principale
et FSquelette pour la gestion des joueurs.
 Module FMain
Le module Fmain contient le point d'entrée de l'exécution
d'un programme de jeux. Il est paramétré par un module de signature
SQUELETTE de description de l'interaction avec un joueur dont
voici la définition.
# module type SQUELETTE = sig 
   val accueil: unit -> unit
   val init: unit -> ((unit -> unit) * (unit -> unit))
   val encore: unit -> bool
   val fin: unit -> unit
   exception Gagne
   exception Perd
   exception Nul
   val gagne: unit -> unit
   val perd: unit -> unit
   val nul: unit -> unit
 end ;;
La fonction init construit un couple des fonctions d'action de chaque joueur. 
Les autres fonctions servent à gérer l'interaction d'une partie.
Le module FMain contient deux fonctions : ca_joue qui fait alterner les joueurs
et main qui gère la boucle principale.
# module FMain (P : SQUELETTE) = 
   struct
     let ca_joue tours = while true do (fst tours) () ; (snd tours) () done
 
     let main () = let fini = ref false 
                   in P.accueil ();
                      while not !fini do
                        ( try  ca_joue (P.init ())
                          with P.Gagne -> P.gagne ()
                             | P.Perd  -> P.perd ()
                             | P.Nul   -> P.nul ()    );
                        fini := not (P.encore ())
                      done ;
                      P.fin ()
   end ;;
module FMain :
  functor(P : SQUELETTE) ->
    sig
      val ca_joue : (unit -> 'a) * (unit -> 'b) -> unit
      val main : unit -> unit
    end
 Module FSquelette
Le module paramétré FSquelette gère les tours de chaque joueur selon 
les indications fournies en début de partie 
comme la nature des joueurs (programme ou non) et l'ordre des joueurs.
Il a besoin de plusieurs paramètres pour 
la représentation du jeu, son affichage, sa fonction d'évaluation et la recherche ab
comme cela est décrit à la figure 17.4.
On donne d'abord la signature manquante pour l'affichage. 
# module type AFFICHAGE = sig 
   type jeu 
   type coup
   val accueil: unit -> unit
   val fin: unit -> unit
   val gagne: unit -> unit
   val perd: unit -> unit
   val nul: unit -> unit
   val init: unit -> unit
   val affiche : bool -> coup -> jeu -> jeu -> unit
   val choix : bool ->  jeu -> coup
   val q_jouer : unit -> bool   
   val q_commencer : unit -> bool
   val q_continuer : unit -> bool
 end ;;
Il est à noter que la représentation du jeu 
et celle des coups doivent être partagées par tous les modules
paramètres, d'où les contraintes sur les types. Les deux fonctions
principales sont tourH et tourM, elles gèrent
respectivement le tour d'un joueur humain (utilisant la fonction
Aff.choix) et celui d'un programme. La fonction init
détermine la nature des joueurs suite aux réponses des appels à
Aff.q_jouer 
# module FSquelette 
     (Rep   : REPRESENTATION) 
     (Aff   : AFFICHAGE with type jeu = Rep.jeu and  type coup = Rep.coup) 
     (Eval  : EVAL with type jeu = Rep.jeu) 
     (Alpha : ALPHABETA with type jeu = Rep.jeu and  type coup = Rep.coup) = 
   struct
     let prof = ref 4
     exception Gagne
     exception Perd
     exception Nul
     let gagne = Aff.gagne
     let perd = Aff.perd
     let nul = Aff.nul
     let encore = Aff.q_continuer
     let le_jeu = ref (Rep.jeu_depart())
     let fin = Aff.fin
     let accueil = Aff.accueil 
 
     let tourH joueur () = 
       let choix = Aff.choix joueur !le_jeu in
       let ancien_jeu = !le_jeu 
       in le_jeu := Rep.jouer joueur choix !le_jeu ;
          Aff.affiche joueur choix ancien_jeu !le_jeu ;
          match Eval.etat_de joueur !le_jeu with
              Eval.P -> raise Perd
            | Eval.G -> raise Gagne
            | Eval.N -> raise Nul
            | _      -> ()
 
     let tourM joueur () = 
       let choix = Alpha.alphabeta !prof joueur !le_jeu in 
       let ancien_jeu = !le_jeu 
       in le_jeu := Rep.jouer joueur choix !le_jeu ;
          Aff.affiche joueur choix ancien_jeu !le_jeu ;
          match Eval.etat_de joueur !le_jeu with
              Eval.G -> raise Gagne
            | Eval.P -> raise Perd
            | Eval.N -> raise Nul
            | _      -> ()
 
     let init () =  
        let a = Aff.q_jouer () in
        let b = Aff.q_jouer() 
        in le_jeu :=Rep.jeu_depart () ;
           Aff.init () ; 
           match (a,b) with  
               true,true   -> tourM true, tourM false
             | true,false  -> tourM true, tourH false
             | false,true  -> tourH true, tourM false
             | false,false -> tourH true, tourH false
   end ;;
Les parties indépendantes du jeu sont maintenant réalisées. On 
peut donc commencer la programmation de différents jeux. Cette
organisation modulaire facilite les modifications de l'affichage ou
de la fonction d'évaluation d'un jeu comme nous allons le voir.
 Puissance 4
On s'intéresse tout d'abord à un premier jeu simple, un morpion
gravitationnel, appelé habituellement Puissance 4. Le jeu est
représenté par sept colonnes de six lignes. 
À chaque tour un joueur pose dans une colonne un jeton de sa couleur, celui-ci 
tombe jusqu'au premier emplacement libre de sa colonne. Si une colonne est complètement remplie, aucun joueur ne peut y jouer. La partie se termine quand 
un des joueurs a relié une ligne de quatre pièces (horizontale,
 verticale, ou diagonale), dans ce cas il a gagné, 
 ou quand toutes les colonnes sont pleines et dans ce cas la partie est nulle.
La figure 17.5 montre une partie finie.
Figure 17.5 : Une partie de Puissance 4
On remarque une ligne de quatre jetons gris en diagonale partant du coin en bas à droite.
 Représentation du jeu : module P4_rep
On choisit pour ce jeu une représentation matricielle. Chaque 
case de la matrice est vide ou bien contient un jeton d'un des deux
joueurs. Un coup est le numéro d'une colonne. Les coups légaux sont
les colonnes dont la dernière case n'est pas remplie. 
# module P4_rep = struct
   type cell = A | B | Vide 
   type jeu = cell array array 
   type coup = int 
   let col = 7 and lig = 6 
   let jeu_depart () = Array.create_matrix lig col Vide 
 
   let coups_legaux b m =   
     let l = ref [] in 
     for c = 0 to col-1 do if m.(lig-1).(c) = Vide then l := (c+1) :: !l done;
     !l
 
   let ajoute mat c =  
     let l = ref lig 
     in while !l > 0 & mat.(!l-1).(c-1) = Vide do  decr l done ; !l + 1
 
   let jouer_gen cp m e =
     let mj = Array.map Array.copy  m 
     in  mj.((ajoute mj cp)-1).(cp-1) <- e ; mj
 
   let jouer b cp m = if b then jouer_gen cp m A else jouer_gen cp m B
 end ;;
On peut facilement vérifier que ce module accepte les contraintes de
la signature 
REPRESENTATION.
# module P4_rep_T = (P4_rep : REPRESENTATION) ;;
module P4_rep_T : REPRESENTATION
 Affichage du jeu : module P4_text
Le module P4_text décrit une interface textuelle pour le jeu Puissance 4 compatible
avec la signature AFFICHAGE. Il ne présente pas de grande difficulté, mais permet de bien 
comprendre ensuite l'assemblage des modules. 
# module P4_text = struct
   open P4_rep
   type jeu = P4_rep.jeu
   type coup = P4_rep.coup
 
   let print_jeu mat = 
     for l = lig - 1 downto 0 do
       for c = 0 to col - 1 do
         match mat.(l).(c) with
             A    -> print_string "X " 
           | B    -> print_string "O "
           | Vide -> print_string ". "
       done; 
       print_newline ()
     done ; 
   print_newline () 
 
   let accueil () = print_string "P4 ...\n"
   let fin () = print_string "A bientôt ... \n"
   let question s = print_string s; print_string " o/n ? " ; read_line() = "o" 
   let q_commencer () = question "Voulez-vous commencer"
   let q_continuer () = question "Encore une partie"
   let q_jouer () = question "Est-ce une machine qui joue ?"
 
   let gagne  ()= print_string "Le 1er joueur gagne" ; print_newline ()
   let perd () = print_string "Le 1er joueur perd" ; print_newline ()
   let nul () = print_string "Partie nulle" ; print_newline ()
   
   let init () = print_string "X: 1er joueur   O: 2eme joueur"; 
                 print_newline () ; print_newline () ;
                 print_jeu (jeu_depart ()) ; print_newline()
 
   let affiche b c aj j = print_jeu j
 
   let est_coup = function '1'..'7' -> true | _ -> false
 
   exception Coup of int
   let rec choix joueur jeu =
     print_string ("Choix joueur" ^ (if joueur then "1" else "2") ^ " : ") ;
     let l = coups_legaux joueur jeu 
     in try  while true do 
               let i = read_line() 
               in ( if (String.length i > 0)  && (est_coup  i.[0])
                    then let c = (int_of_char i.[0]) - (int_of_char '0') 
                         in if List.mem c l then raise (Coup c) );
                  print_string "coup non valable, essayez encore : "
             done ; 
             List.hd l
        with  Coup i -> i 
          |    _     -> List.hd l
 end ;;
On vérifie immédiatement qu'il respecte bien les contraintes de la signature AFFICHAGE.
# module P4_text_T = (P4_text : AFFICHAGE) ;;
module P4_text_T : AFFICHAGE
 Fonction d'évaluation : module P4_eval
 La qualité d'un jeu dépend
principalement de la fonction d'évaluation d'une position. Le module P4_eval définit 
la fonction evaluer qui calcule la valeur d'une position pour un joueur donné. Cette fonction
appelle la fonction eval_bloc, dans les quatre directions en spécialisant
les diagonales, qui elle-même appelle la fonction eval_quatre pour calculer le nombre de
jetons dans la ligne demandée. 
Le tableau valeur donne la valeur d'un bloc contenant 0, 1, 2
ou 3 jetons de la même couleur. L'exception Quatre est
déclenchée quand 4 jetons sont alignés. 
# module P4_eval = struct
   open P4_rep
   type jeu = P4_rep.jeu
   let valeur = Array.of_list [0; 2; 10; 50] 
   exception Quatre of int 
   exception Valeur_nulle 
   exception Arg_invalid 
   let moinsI = -10000
   let plusI = 10000
 
   let eval_quatre m l_dep c_dep delta_l delta_c =
     let n = ref 0 and e = ref Vide 
     and x = ref c_dep  and  y = ref l_dep 
     in try 
          for i = 1 to 4 do 
            if !y<0 or !y>=lig or !x<0 or !x>=col then raise Arg_invalid ; 
            ( match m.(!y).(!x) with 
                  A    -> if !e = B then raise Valeur_nulle ;
                          incr n ;
                          if !n = 4 then raise (Quatre plusI) ;
                          e := A
                | B    -> if !e = A then raise Valeur_nulle ;
                          incr n ;
                          if !n = 4 then raise (Quatre moinsI);
                          e := B; 
                | Vide -> ()                                      ) ;
            x := !x + delta_c ;
            y := !y + delta_l  
          done ; 
          valeur.(!n) * (if !e=A then 1 else -1)
        with 
            Valeur_nulle | Arg_invalid  -> 0
 
   let eval_bloc m e cmin cmax lmin lmax dx dy = 
     for c=cmin to cmax do for l=lmin to lmax do 
         e := !e + eval_quatre m l c dx dy
     done done
 
   let evaluer b m = 
   try let evaluation = ref 0 
       in (* evaluation des lignes *)
          eval_bloc m evaluation 0 (lig-1) 0 (col-4) 0 1 ;
          (* evaluation des colonnes *)
          eval_bloc m evaluation 0 (col-1) 0 (lig-4) 1 0 ;
         (* diagonales partant de la premiere ligne (à droite) *)
         eval_bloc m evaluation 0 (col-4) 0 (lig-4) 1 1 ;
         (* diagonales partant de la premiere colonne (à droite) *)
         eval_bloc m evaluation 1 (lig-4) 0 (col-4) 1 1 ;
         (* diagonales partant de la premiere ligne (à gauche) *)
         eval_bloc m evaluation 3 (col-1) 0 (lig-4) 1 (-1) ;
         (* diagonales partant de la derniere colonne (à gauche) *)
         eval_bloc m evaluation 1 (lig-4) 3 (col-1) 1 (-1) ;
         !evaluation
   with Quatre v -> v
 
   let est_feuille b m = let v = evaluer b m 
                         in v=plusI or v=moinsI or coups_legaux b m = []
 
   let est_stable b j = true
 
   type etat = G | P | N | C 
 
   let etat_de joueur m =
     let v = evaluer joueur m 
     in if v = plusI then if joueur then G else P
        else if v = moinsI then if joueur then P else G
        else if coups_legaux joueur m = [] then N else C
 end ;;
Le module P4_eval est compatible avec les contraintes de la signature EVAL
# module P4_eval_T = (P4_eval : EVAL) ;;
module P4_eval_T : EVAL
Pour faire jouer deux fonctions d'évaluation l'une contre l'autre, il
faut modifier la fonction evaluer
en aiguillant chaque joueur sur sa fonction d'évaluation propre. 
 Assemblage des modules
 Toutes les briques nécessaires à la réalisation du jeu puissance 4 sont maintenant réalisées.
Il ne reste plus qu'à les assembler selon le schéma de la figure
17.4. 
On construit tout d'abord le squelette P4_Squelette qui est l'application du module paramétré FSquelette
aux modules P4_rep, P4_text, P4_eval et au résultat de l'application du module 
paramétré FAlphabeta sur P4_rep et P4_eval.
# module P4_squelette = 
   FSquelette (P4_rep) (P4_text) (P4_eval)  (FAlphabeta (P4_rep) (P4_eval)) ;;
module P4_squelette :
  sig
    val prof : int ref
    exception Gagne
    exception Perd
    exception Nul
    val gagne : unit -> unit
    val perd : unit -> unit
    val nul : unit -> unit
    val encore : unit -> bool
    val le_jeu : P4_text.jeu ref
    val fin : unit -> unit
    val accueil : unit -> unit
    val tourH : bool -> unit -> unit
    val tourM : bool -> unit -> unit
    val init : unit -> (unit -> unit) * (unit -> unit)
  end
On obtient alors le module principal P4_main de l'application du module paramétré FMain sur
le résultat de l'application précédente P4_squelette.
# module P4_main = FMain(P4_squelette) ;;
module P4_main :
  sig
    val ca_joue : (unit -> 'a) * (unit -> 'b) -> unit
    val main : unit -> unit
  end
Le lancement du jeu s'effectue par l'application de la fonction P4_main.main sur ().
 Test du jeu
 Tel que le squelette général des jeux a été écrit, il est possible 
de faire jouer deux personnes entre elles (le programme ne sert que d'arbitre), une personne contre le programme 
ou le programme contre lui-même. Bien que cette dernière possibilité ne présente que peu d'intérêt pour un joueur,
elle permet de lancer des tests plus facilement sans devoir attendre la réponse d'un joueur.
Le jeu suivant choisit cette option. 
# P4_main.main ();;
P4 ...
Est-ce une machine qui joue ? o/n ? o
Est-ce une machine qui joue ? o/n ? o
X: 1er joueur   O: 2eme joueur
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . . . 
Une fois la position initiale affichée, le joueur 1 (joué par le programme) calcule
 son coup qui est ensuite affiché.
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . . . 
. . . . . X . 
Le joueur 2 (toujours joué par le programme) calcule son propre coup et le jeu s'affiche.
Le jeu se poursuit ainsi de suite jusqu'à une position de fin de partie. Dans le cas présent le joueur 1 gagne la partie sur la position finale suivante :
. O O O . O . 
. X X X . X . 
X O O X . O . 
X X X O . X . 
X O O X X O . 
X O O O X X O 
Le 1er joueur gagne
Encore une partie o/n ? n
A bientot ... 
- : unit = ()
 Interface graphique
Comme le plaisir de jouer provient aussi de l'interface graphique du
programme, on définit un nouveau module P4_graph compatible
avec la signature AFFICHAGE qui ouvre une 
fenêtre graphique et gère les clics souris. On retrouve le texte de ce module dans le sous-catalogue
Applications du CD-ROM (voir page ??).
On crée alors un nouveau squelette (P4_squeletteG) qui
résulte de l'application du module paramétré FSquelette.
# module P4_squeletteG = 
   FSquelette (P4_rep) (P4_graph) (P4_eval) (FAlphabeta (P4_rep) (P4_eval)) ;;
Seul le paramètre du module d'affichage diffère de la version texte
dans l'application du module paramétré FSquelette. On crée
alors le module principal de l'application Puissance 4 avec interface
graphique. 
# module P4_mainG = FMain(P4_squeletteG) ;;
module P4_mainG :
  sig
    val ca_joue : (unit -> 'a) * (unit -> 'b) -> unit
    val main : unit -> unit
  end
L'évaluation de l'expression P4.mainG.main() affiche la
fenêtre graphique de la figure 17.5 et gère l'interaction
avec le joueur.
 Stone Henge
Le jeu Stone Henge, dû à Reiner Knizia, est un jeu de construction de lignes de force. 
Les règles sont simples à apprendre, mais l'intérêt du jeu reste grand même après 
de nombreuses parties. La règle en ligne peut être consultée au lien suivant : 
 Lien 
 
http://www.gamecabinet.com/frenchRules/Stonehenge.html
La position initiale du jeu est représentée à la figure 17.6.
 Présentation du jeu
Le but du jeu est de gagner au moins 8 lignes de force (lignes claires) sur les 15 existantes. Le 
gain d'une ligne est visualisé par la pose d'un menhir (ou mégalithe) sur une case gris foncé associée à une ligne de force. 
Figure 17.6 : Position initiale de Stone Henge
À son tour chaque joueur pose une pierre (sur les 9 qu'il possède au début) numérotée (de 1 à 6) 
sur une des 18 cases centrales grises. On ne peut pas poser une pierre sur une case occupée. 
Une fois une pierre posée, une ou plusieurs lignes de force peuvent être perdues ou gagnées.
Une 
ligne de force est gagnée par un joueur si l'autre joueur ne peut pas dépasser le 
score (total des pierres déjà posées d'une couleur) 
du premier, même en remplissant les cases encore vides de cette ligne par ses meilleures pierres. 
Par exemple, à la figure 17.7, le joueur noir commence par poser la pierre de valeur 3,
le joueur rouge sa pierre 2, puis le joueur noir joue sa pierre 6 et gagne une ligne. Le joueur rouge
 pose alors sa pierre 4 et gagne aussi une ligne de force. Celle-ci
n'est pas encore complètement remplie, mais le joueur noir ne pourra
jamais dépasser le score de son adversaire. 
On peut noter que le joueur rouge aurait pu jouer une pierre 3 à la place de sa pierre 4. En effet il reste une seule case libre sur cette ligne de force et la pierre la plus forte de noir vaut 5, donc noir
ne peut plus battre rouge sur cette ligne. 
Figure 17.7 : Position après 4 coups
Dans un cas d'égalité sur une ligne pleine, celui qui a posé la dernière pierre et qui n'a pas pu battre le score de son adversaire perd cette ligne. La figure 17.8 montre un tel cas. 
Le dernier coup de rouge est la pierre 4. Sur la ligne pleine où le 4 est posé il y a égalité. Comme rouge est le dernier joueur a y avoir posé une pierre, il n'a pas pu battre son adversaire et perd la ligne,
d'où le menhir noir qui indique le gain de noir. On s'aperçoit que la fonction jouer qui effectue
le rôle d'arbitre doit tenir compte de ces subtilités de pose de menhirs.
Figure 17.8 : Position après 6 coups
Il n'y a jamais de partie nulle à ce jeu. Il y a 15 lignes et toutes sont prises à un moment de la partie,
donc un des deux joueurs atteint le gain d'au moins 8 lignes.
 Complexité de la recherche
Avant toute implantation d'un nouveau jeu, il est nécessaire de calculer le 
nombre de coups légaux 
entre deux tours de jeu ainsi que le nombre de coups d'une partie. 
On détermine alors la profondeur raisonnable de l'algorithme minimax-ab.
Dans le jeu StoneHenge le nombre de coups 
d'une partie est borné par le nombre de jetons des deux joueurs, c'est-à-dire 18. Le nombre de coups possibles
diminue au fur et à mesure de la partie. Au premier coup de la partie, le 
 joueur a 6 jetons différents et 18 cases libres, ce qui lui fait 108 coups légaux. Au deuxième coup, 
le deuxième joueur a aussi 6 jetons différents pour 17 cases libre (102 coups légaux). Le fait de passer d'une profondeur 
de 2 à 4 pour les premiers coups de la partie fait passer d'environ 104 coups à 108 coups. 
Par contre en fin de partie, disons sur les 8 derniers coups, la complexité se réduit fortement.
Si on calcule le pire des cas (tous les jetons sont différents) on obtient environ 23 millions de possibilités : 
 4*8 * 4*7 * 3*6 * 3*5 * 2*4 * 2*3 * 1*2 * 1*1 = 23224320
Il semble délicat de calculer au delà d'une profondeur 2 pour les premiers coups. 
Cela dépend de la fonction d'évaluation, et de sa capacité à évaluer les positions de début de partie, du moins
tant qu'il y a peu de menhirs posés. 
Par contre en fin de partie la profondeur peut augmenter à 4 ou 6, mais cela 
est probablement trop tard pour récupérer une position affaiblie.
 Implantation
La réalisation de ce jeu suit l'organisation utilisée pour le jeu Puissance 4 et décrite à la 
figure 17.4. Les deux principales 
difficultés viennent du respect des règles du jeu pour la pose des menhirs et de la fonction d'évaluation qui doit pouvoir évaluer une situation le plus rapidement possible tout en étant pertinente.
On décrit tout d'abord la représentation du jeu et l'arbitre de jeu, pour s'intéresser par la suite à la fonction d'évaluation.
 Représentation du jeu
Il y a quatre données particulières pour ce jeu : 
- 
 les pierres (ou jetons) des joueurs (type pierre)
-  les menhirs (type menhir)
-  les 15 lignes de force 
-  les 18 cases où poser les pierres
On donne un numéro unique à chaque case.
              1---2
             / \ / \
           3---4---5
          / \ / \ / \
         6---7---8---9
        / \ / \ / \ / \
       10--11--12--13--14
        \ / \ / \ / \ /
         15--16--17--18 
Par chaque case passe 3 lignes de force. On numérote aussi chaque ligne de force.
Cette description se retrouve dans la déclaration de la liste lignes, que l'on convertit 
en vecteur (vecteur_l). Une case est soit vide, soit contient la pierre posée et son propriétaire.
On conserve d'autre part pour chaque case le numéro des lignes qui y passent. 
Ce tableau est calculé par la fonction 
lignes_par_case et est nommé num_ligne_par_case. 
Le jeu est représenté par le vecteur des 18 cases, le vecteur des 15 lignes de force contenant ou non un menhir,
et des listes de jetons pour les deux joueurs. La fonction jeu_depart crée ces quatre
 éléments.
Le calcul des coups légaux pour un joueur revient à un produit cartésien des jetons restants avec les cases libres.
Plusieurs fonctions utilitaires permettent de compter le score d'un joueur dans une ligne, de calculer
le nombre de cases libres d'une ligne et de vérifier si une ligne a déjà un menhir. Il ne reste plus qu'à écrire
la fonction jouer qui joue un coup et décide des menhirs à poser. Nous la décrivons à la fin du 
listing suivant du module Stone_rep.
# module Stone_rep = struct
 type joueur = bool 
 type pierre =  P of int
 let int_of_pierre = function P x -> x 
 
 type menhir = Rien | M of joueur
 
 type case = Vide | Occup of joueur*pierre
 let value_on_case = function
   Vide -> 0
 | Occup (j, x) -> int_of_pierre x
 
 type jeu = J of case array * menhir array * pierre list * pierre list
 type coup =  int * pierre 
 
 let lignes = [
   [0;1]; [2;3;4]; [5; 6; 7; 8;]; [9; 10; 11; 12; 13]; [14; 15; 16; 17];
   [0; 2; 5; 9]; [1; 3; 6; 10; 14]; [4; 7; 11; 15]; [8; 12; 16]; [13; 17];
   [9; 14]; [5; 10; 15]; [2; 6; 11; 16]; [0; 3; 7; 12; 17]; [1; 4; 8; 13]  ]
 
 let vecteur_l = Array.of_list lignes
 
 let lignes_par_case v =
   let t =  Array.length v  in
   let r = Array.create 18 [||] in
     for i = 0 to 17  do
       let w = Array.create 3 0
       and p = ref 0 in
         for j=0 to t-1 do if List.mem i v.(j) then (w.(!p) <- j; incr p)
         done;
         r.(i) <- w
     done;
     r
 
 let num_ligne_par_case = lignes_par_case vecteur_l
 let rec lignes_de_i i l = List.filter (fun t -> List.mem i t) l 
  
 let lignes_des_cases l =
   let a = Array.create 18 l in
     for i=0 to 17 do
       a.(i) <-  (lignes_de_i i l)
     done; a
 
 let ldc = lignes_des_cases lignes
 
 let jeu_depart ()= let lp = [6; 5; 4; 3; 3; 2; 2; 1; 1] in 
    J ( Array.create 18 Vide, Array.create 15 Rien,
        List.map (fun x -> P x) lp, List.map (fun x -> P x) lp )
 
 let rec unicite l = match l with
   [] -> []
 | h::t -> if List.mem h t then unicite t else h:: (unicite t)
 
   let coups_legaux joueur (J (ca, m, r1, r2)) =
     let r = if joueur then r1 else r2 in
       if r = [] then []
       else
         let l = ref [] in
           for i = 0 to 17 do
             if value_on_case ca.(i) = 0 then l:= i:: !l
           done;
           let l2 = List.map (fun x-> 
                     List.map (fun y-> x,y) (List.rev(unicite r)) ) !l in
           List.flatten l2
   let copie_plateau p = Array.copy p
 
   let  copie_carnac m = Array.copy m
    let rec joue_pierre caillou l = match l with
      [] -> []
    | x::q -> if x=caillou  then q
              else x::(joue_pierre caillou q)
 
 let compte_case joueur case = match case with
   Vide -> 0
 | Occup (j,p) -> if j = joueur then (int_of_pierre p) else  0
 
 let compte_ligne joueur ligne pos =
    List.fold_left (fun x y -> x +  compte_case joueur pos.(y)) 0 ligne
 
     let rec compte_max n = function
         [] -> 0
       | t::q ->
           if (n>0) then
             (int_of_pierre t) + compte_max (n-1) q
           else
             0
 
 let rec nbr_cases_libres ca  l  = match l with
   [] -> 0
 | t::q -> let c = ca.(t) in
     match c with
          Vide -> 1 + nbr_cases_libres ca q
         |_ -> nbr_cases_libres ca q
 
 let a_menhir i ma =
   match ma.(i) with
     Rien -> false
   | _ -> true
 
 let quel_menhir i ma =
   match ma.(i) with
     Rien -> failwith "quel_menhir"
   | M j  -> j
 
 let est_pleine l ca =  nbr_cases_libres ca l = 0
 
 (* fonction jouer : arbitre du jeu *)
 let jouer joueur coup jeu =
     let (c, i) = coup in
         let J (p, m, r1, r2) = jeu in
         let nr1,nr2 = if joueur then joue_pierre i r1,r2 
                       else r1,  joue_pierre i r2  in
         let np = copie_plateau p in
         let nm = copie_carnac m in
           np.(c)<-Occup(joueur,i);  (* on joue le coup *)
           let lignes_de_la_case = num_ligne_par_case.(c) in
 
           (* calcul des menhirs des 3 lignes de la case *)
           for k=0 to 2 do
           let l =  lignes_de_la_case.(k) in
               if not (a_menhir l nm) then (
                 if est_pleine vecteur_l.(l) np then (
                   let c1 = compte_ligne joueur vecteur_l.(l) np
                   and c2 = compte_ligne (not joueur) vecteur_l.(l) np in
                     if (c1 > c2) then nm.(l) <- M joueur
                     else  ( if c2 > c1 then nm.(l) <- M (not joueur)
                             else nm.(l) <- M (not joueur ))))
           done;
 
          (* calcul des autres menhirs *)
          for k=0 to 14 do
            if not (a_menhir k nm ) then
              if est_pleine vecteur_l.(k) np then failwith "joueur"
              else   
                let c1 = compte_ligne joueur  vecteur_l.(k) np
                and c2 = compte_ligne (not joueur) vecteur_l.(k) np in
                let cases_libres = nbr_cases_libres np vecteur_l.(k) in
                let max1  = compte_max cases_libres 
                              (if joueur then nr1 else nr2)
                and max2 = compte_max cases_libres 
                              (if joueur then nr2 else nr1) in
                if c1 >= c2 + max2 then nm.(k) <- M joueur
                else if c2 >= c1 + max1 then nm.(k) <- M (not joueur)
        done;
        J(np,nm,nr1,nr2)
 end;;
La fonction jouer se découpe en 3 étapes : 
- 
 la recopie du jeu et la pose du coup; 
-  la détermination de la pose d'un menhir sur une des trois lignes de la case jouée;
-  le traitement des autres lignes de force.
La deuxième étape vérifie pour chacune des 3 lignes qui passent
 par cette case si elle n'a pas
déjà un menhir, puis si elle est pleine. Dans ce cas elle fait le compte pour chaque
joueur et détermine lequel est le plus grand strictement et attribue le menhir à ce joueur là. En 
cas d'égalité, la ligne va au joueur adverse de celui qui vient de jouer. En effet, il n'existe pas
de ligne avec une seule case. Une ligne pleine a au moins deux pierres. Donc le joueur qui vient
de jouer a juste égalisé le score de son adversaire. Il ne peut pas prétendre gagner cette ligne qui va à son adversaire. Si la ligne traitée n'est pas pleine, elle sera analysée par l'étape 3.
La troisième étape vérifie pour chaque ligne non encore attribuée, ce qui indique par là 
que la ligne n'est pas pleine, si un joueur ne peut plus être battu par son adversaire. 
Dans ce cas, la ligne lui est immédiatement donnée. Pour réaliser ce test, il est nécessaire 
de calculer le score maximal potentiel d'un joueur sur une ligne 
(c'est à dire en utilisant ses meilleurs pierres). Si la ligne est encore en discussion rien ne se passe.
 Évaluation
La fonction d'évaluation doit rester simple devant le nombre de cas à traiter
en début de jeu. L'idée est de ne pas trop simplifier le jeu en jouant immédiatement ses
pierres les plus fortes, ce qui laisserait le champ libre à l'adversaire. 
Deux critères sont utilisés : le nombre de menhirs gagnés et la potentialité 
des futurs coups en calculant la valeur des pierres restantes. On utilise la formule 
suivante pour le joueur 1 : 
 
score   =   50 * (c1 - c2) + 10 * (pr1 - pr2)
où ci est le nombre de menhirs et pri la somme des pierres restantes du joueur i.
La formule retourne un résultat positif si les différences des menhirs (c1 - c2) 
et des potentialités (pr1 - pr2) sont à l'avantage du joueur 1.
On s'aperçoit alors que la pose de la pierre 6 n'est effectuée que si elle rapporte au moins deux lignes.
Le gain d'une ligne donne 50, mais le fait de jouer 6 fait perdre 10*6 points, on préfère alors jouer 1 qui calcule le même score (perte de 10 points).
# module Stone_eval = struct
 open Stone_rep
 type jeu = Stone_rep.jeu
  
 exception Fini of bool
 let plusI = 1000 and  moinsI = -1000
  
 let nbr_lignes_gagnees  (J(ca, m,r1,r2))  =
   let c1,c2 = ref 0,ref 0 in
     for i=0 to 14 do
       if a_menhir i m then if quel_menhir i m then incr c1 else incr c2
     done;
     !c1,!c2
 
 let rec nbr_points_restant lig = match lig with 
     [] -> 0
 | t::q -> (int_of_pierre t) + nbr_points_restant q
 
 let evaluer joueur jeu  =
   let (J (ca,ma,r1,r2)) = jeu in
   let c1,c2  = nbr_lignes_gagnees  jeu in
   let pr1,pr2 = nbr_points_restant r1, nbr_points_restant r2 in
      match joueur with
        true -> if c1 > 7 then plusI else 50 * (c1 - c2) + 10 * (pr1 - pr2)
      | false -> if c2 > 7 then moinsI else 50 * (c1 - c2) + 10 * (pr1 - pr2)
 
 let est_feuille joueur jeu =
   let v = evaluer joueur jeu  in
     v = plusI or v = moinsI or coups_legaux joueur jeu  = []
 
 let est_stable joueur jeu = true
 
 type etat = G | P | N | C
 let etat_de joueur m =
   let v = evaluer joueur m in
   if v = plusI then if joueur then G else P
   else 
     if v = moinsI 
     then if joueur then P else G
     else 
       if coups_legaux joueur  m = [] then  N else C
 end;;
 Assemblage
On écrit par ailleurs le module Stone_graph qui décrit une 
interface graphique compatible avec la signature AFFICHAGE.
On construit Stone_squeletteG à la manière 
de P4_squeletteG en passant les arguments propres au jeu Stone Henge
à l'application du module paramètre FSquelette.
# module Stone_squeletteG = FSquelette (Stone_rep)
                                      (Stone_graph)
                                      (Stone_eval)
                                      (FAlphabeta (Stone_rep) (Stone_eval)) ;;
module Stone_squeletteG :
  sig
    val prof : int ref
    exception Gagne
    exception Perd
    exception Nul
    val gagne : unit -> unit
    val perd : unit -> unit
    val nul : unit -> unit
    val encore : unit -> bool
    val le_jeu : Stone_graph.jeu ref
    val fin : unit -> unit
    val accueil : unit -> unit
    val tourH : bool -> unit -> unit
    val tourM : bool -> unit -> unit
    val init : unit -> (unit -> unit) * (unit -> unit)
  end
On construit alors le module principal Stone_mainG.
# module Stone_mainG = FMain(Stone_squeletteG);;
module Stone_mainG :
  sig
    val ca_joue : (unit -> 'a) * (unit -> 'b) -> unit
    val main : unit -> unit
  end
Le lancement de Stone_mainG.main () ouvre la fenêtre de la figure 17.6. Après 
le dialogue pour savoir qui joue,
 la partie commence. Un joueur humain sélectionne d'abord la pierre à jouer
puis la case où la poser. 
 Pour en faire plus
L'organisation de ces applications utilisant plusieurs modules paramétrés permet de réutiliser pleinement
le module FAlphabeta et le module FSquelette pour les deux jeux que nous avons décrits. Néanmoins 
pour le jeu Stone Henge certaines fonctions du module Stone_rep, nécessaires
 pour la fonction jouer, qui n'apparaissent
pas dans la signature REPRESENTATION ont été utilisées par la fonction d'évaluation. C'est pour cela
que le module Stone_rep n'a pas été immédiatement fermé par la signature REPRESENTATION.
Cette ouverture entre modules pour la partie spécifique des jeux permet un développement plus incrémentiel
sans fragiliser le schéma de dépendances des modules présenté à la figure 17.4.
Une première amélioration concerne les jeux où d'une position et d'un coup il est facile de déterminer la position
précédente. Dans ces cas là, il peut être plus efficace de ne pas effectuer une copie du jeu par la fonction jouer, mais de conserver un historique des coups joués pour pouvoir revenir en arrière (backtraking). C'est le cas 
de Puissance 4 mais pas de Stone Henge. 
Une deuxième amélioration est de profiter du temps d'attente de la réponse d'un joueur pour commencer l'évaluation 
des futures positions. 
Pour cela on peut utiliser des processus légers (voir chapitre 19) qui
lancent le calcul pendant cette attente. Si la réponse fait partie du début de l'exploration des positions, alors le gain de temps est immédiat, sinon on repart avec la nouvelle position.
Une troisième amélioration est de constituer et d'exploiter des
bibliothèques d'ouvertures. Nous avons pu constater avec Stone
Henge, mais c'est aussi le cas pour beaucoup d'autres jeux, que l'éventail
des possibilités et donc le nombre des coups légaux à explorer
est plus important en début de partie. Il y a donc tout à gagner
à précalculer les meilleurs coups des positions de départ et à les
sauver dans un fichier. On peut enrichir l'intérêt du jeu, en faisant
intervenir le hasard pour choisir parmi un certain nombre de variantes
menant à des positions dont les valeurs sont proches.
Une quatrième voie est de ne pas borner la profondeur de la
recherche par une valeur fixe, mais par un temps de calcul à ne pas
dépasser. De la sorte, le programme devient plus efficace en fin de
partie quand l'éventail des choix se retrouve limité. Cette
modification demande de modifier quelque peu l'implantation de 
minmax afin de pouvoir reprendre un arbre pour augmenter sa
profondeur. Une heuristique dépendant du jeu, donc paramètre de
minmax pourrait choisir quelles sont les branches dont
l'exploration doit être poussée et quelles sont celles qui
peuvent être abandonnées.
Il existe d'autre part de nombreux autres jeux qui ne demandent qu'à être implantés ou réimplantés. 
On peut citer plusieurs classiques : Dames, Othello, Abalone, ..., 
mais aussi de nombreux jeux moins connus et
néanmoins fort jouables. On trouve au lien suivant quelques projets d'étudiants
comme le jeu de Dames ou le jeu Nuba.
 Lien 
 
http://www.pps.jussieu.fr/~emmanuel/Public/enseignement/
Les jeux avec tirage aléatoire (pioche des jeux de cartes, lancement de dés) 
nécessitent une modification de l'algorithme minimax-ab
pour tenir compte de la probabilité de ce tirage. 
Nous reviendrons sur les interfaces des jeux au chapitre
21 en construisant des interfaces pour les
navigateurs WWW apportant sans frais la fonction retour au dernier
coup. On appréciera d'autant plus l'organisation par module, qui
permet de ne modifier qu'un élément, ici l'affichage et l'interaction,
d'une application de jeux à 2 joueurs. 
 
 
