Précédent Index Suivant

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 : 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 : À 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 : 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 :
  1. la recopie du jeu et la pose du coup;
  2. la détermination de la pose d'un menhir sur une des trois lignes de la case jouée;
  3. 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)
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.




Précédent Index Suivant