IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

L'approche qualité avec OCaml

Image non disponible


précédentsommairesuivant

V. Partie IV

  1. Le jeu des tétrominos
  2. Le tableau de jeu
  3. La génération des coups
  4. Le calcul de la valeur d'un tableau

V-A. Application à la théorie des jeux

V-A-1. 29. Le jeu des tétrominos

Vous connaissez déjà les pièces, voici maintenant le jeu.

On énonce en six points la règle du jeu de tétrominos (à deux joueurs) :

  • chaque joueur dispose de cinq tétrominos ;
  • chaque joueur dépose tour à tour une de ses cinq pièces sur une grille rectangulaire de 5 x 8 cases ;
  • avant la pose une pièce peut être librement orientée et retournée ;
  • après la pose une pièce est inamovible et conserve sa position jusqu'à la fin de la partie
  • le premier joueur qui ne peut plus poser une de ses pièces a perdu ;
  • si la grille est totalement couverte par les dix pièces des deux joueurs alors la partie est déclarée nulle.
Image non disponible

On appelle valeur théorique du jeu celle qui est vraie parmi ces trois valeurs possibles :

  • first wins : il existe une stratégie gagnante pour le premier joueur ;
  • second wins : il existe une stratégie gagnante pour le deuxième joueur ;
  • tie game : deux joueurs « parfaits » font toujours une partie nulle.

On se propose, avec nos connaissances en OCaml, de calculer cette valeur théorique en trois étapes.

Mais d'abord, faisons plusieurs observations qui découlent directement de la règle du jeu :

  • le plateau de jeu contient 40 cases ;
  • la durée d'une partie est d'au maximum cinq coups par joueur ;
  • la valeur théorique du jeu dépend uniquement de trois paramètres qui sont : la main de chaque joueur, de l'occupation du plateau de jeu et, bien sûr (puisque c'est là toute la question de la valeur théorique) de qui a la main ;
  • en particulier la valeur théorique du jeu ne dépend pas de la couleur des cases occupées.

Précisons notre vocabulaire :

  • avoir la main signifie avoir l'initiative de jouer ;
  • la main d'un joueur est l'ensemble des pièces que ce joueur n'a pas encore posées sur le plateau de jeu ;
  • l'occupation d'une case ne peut prendre que deux valeurs : libre ou occupée ;
  • la couleur d'une case occupée ne peut prendre que deux valeurs : bleue ou rouge ;
  • le premier joueur (first) désigne celui qui a la main, le second joueur (second) désigne celui qui n'a pas la main ;
  • chaque joueur est alternativement first/second.

V-A-2. 30. Le tableau de jeu

D'une part nous appellerons tableau de jeu la structure de donnée choisie pour représenter les trois paramètres dont dépend la valeur théorique du jeu.

D'autre part il faut savoir qu'un entier OCaml (du type int) contient au minimum 31 bits. Il nous faudra donc deux entiers pour représenter le plateau de jeu si un bit à 0 désigne une case vide et un bit à 1 désigne une case occupée.

Nous choisissons de couper le plateau de jeu en deux parties, horizontalement, de sorte que 20 bits représenteront les 20 cases supérieures du plateau et 20 autres bits représenteront les 20 cases inférieures du plateau.

Du coup, dans chacun des deux entiers, il reste encore 11 bits à notre disposition. Or il nous suffit de cinq bits pour représenter une main si un bit à 0 désigne une pièce déjà posée et un bit à 1 désigne une pièce dans la main. Assez opportunément le tableau de jeu (ou bitboard) sera donc constitué de deux entiers, le premier contiendra la main du premier joueur et la partie supérieure du plateau, le second contiendra la main du second joueur et la partie inférieure du plateau. La notation 0bBBBBB désigne un nombre de cinq bits en numérotation binaire. Les informations de main sont placées à partir du 20e bit pour laisser assez de place pour le masque d'occupation du plateau.

 
Sélectionnez
type hand = int;; 

let hand_i = 0b00001 lsl 20
and hand_l = 0b00010 lsl 20
and hand_o = 0b00100 lsl 20
and hand_s = 0b01000 lsl 20
and hand_t = 0b10000 lsl 20
;;

let hand_mask  = 0x1F00000
and board_mask = 0x00FFFFF
;;

type bitboard = {first: int; second: int};;
type piece    = {upper: int; lower:  int};;

Le type piece désigne les cases occupées par une certaine pièce posée à une certaine position, il est identique au type bitboard excepté qu'il ne contient pas d'information de main.

Au début du jeu les joueurs possèdent toutes les pièces et aucune case n'est occupée, le tableau de jeu initial est donc :

 
Sélectionnez
{first = hand_mask; second = hand_mask};;

Pour chacun des tétrominos I.L.O.S.T. nous avons besoin de la liste de toutes ses positions possibles sur le plateau de jeu.

D'abord, étant donné une pièce p, la fonction unfold_piece p a b c génère la liste de toutes les positions possibles de cette pièce sur le plateau, avec a une liste de décalages pour lesquels p reste dans la partie supérieure du plateau, b une liste de décalages pour lesquels p est à cheval sur la partie supérieure et la partie inférieure, et enfin c une liste de décalages pour lesquels p reste dans la partie inférieure du plateau :

 
Sélectionnez
let unfold_piece p a b c =
  let lower_part n =
    if n >= 20 then p lsr (n - 20)
    else (p lsl (20 - n)) land board_mask
  in List.flatten [
    List.map (fun n -> {upper = p lsr n; lower = 0}) a;
    List.map (fun n -> {upper = p lsr n; lower = lower_part n}) b;
    List.map (fun n -> {upper = 0; lower = p lsr (n - 20)}) c;
  ];;

Ensuite, en fournissant les listes a b c adéquates, on spécialise unfold_piece pour des pièces de largeur 3 et de hauteur 2, des pièces de largeur 2 et de hauteur 3, la pièce de largeur 1 et de hauteur 4, la pièce de largeur 4 et de hauteur 1, et enfin de la pièce carrée de côté 2 :

 
Sélectionnez
let unfold_piece_3x2 p =
  unfold_piece p
  [0;1;2; 5;6;7; 10;11;12] [15;16;17] [20;21;22; 25;26;27; 30;31;32]
and unfold_piece_2x3 p =
  unfold_piece p
  [0;1;2;3; 5;6;7;8] [10;11;12;13; 15;16;17;18] [20;21;22;23; 25;26;27;28]
and unfold_piece_1x4 p =
  unfold_piece p
  [0;1;2;3;4] [5;6;7;8;9; 10;11;12;13;14; 15;16;17;18;19] [20;21;22;23;24]
and unfold_piece_4x1 p =
  unfold_piece p
  [0;1; 5;6; 10;11; 15;16] [] [20;21; 25;26; 30;31; 35;36]
and unfold_piece_2x2 p =
  unfold_piece p
  [0;1;2;3; 5;6;7;8; 10;11;12;13] [15;16;17;18] [20;21;22;23; 25;26;27;28; 30;31;32;33]
;;

On appelle alors:

  • j la pièce qui miroite la pièce l ;
  • z la pièce qui miroite la pièce s ;
  • 0 la rotation d'angle nul ;
  • 1 la rotation d'un quart de tour dans le sens horaire ;
  • 2 la rotation de deux quarts de tour dans le sens horaire ;
  • 3 la rotation de trois quarts de tour dans le sens horaire.

On obtient la liste de toutes les positions possibles des 19 tétrominos orientés à l'aide de ces 19 déclarations :

 
Sélectionnez
let tetro_i0 = unfold_piece_1x4 0b10000100001000010000
and tetro_i1 = unfold_piece_4x1 0b11110000000000000000
and tetro_l0 = unfold_piece_2x3 0b10000100001100000000
and tetro_l1 = unfold_piece_3x2 0b11100100000000000000
and tetro_l2 = unfold_piece_2x3 0b11000010000100000000
and tetro_l3 = unfold_piece_3x2 0b00100111000000000000
and tetro_j0 = unfold_piece_2x3 0b01000010001100000000
and tetro_j1 = unfold_piece_3x2 0b10000111000000000000
and tetro_j2 = unfold_piece_2x3 0b11000100001000000000
and tetro_j3 = unfold_piece_3x2 0b11100001000000000000
and tetro_oo = unfold_piece_2x2 0b11000110000000000000
and tetro_s0 = unfold_piece_3x2 0b01100110000000000000
and tetro_s1 = unfold_piece_2x3 0b10000110000100000000
and tetro_z0 = unfold_piece_3x2 0b11000011000000000000
and tetro_z1 = unfold_piece_2x3 0b01000110001000000000
and tetro_t0 = unfold_piece_3x2 0b11100010000000000000
and tetro_t1 = unfold_piece_2x3 0b01000110000100000000
and tetro_t2 = unfold_piece_3x2 0b01000111000000000000
and tetro_t3 = unfold_piece_2x3 0b10000110001000000000
;;

Déclarations dans lesquelles 0bBBBBBBBBBBBBBBBBBBBB est un nombre en binaire, sur 20 bits distribués en quatre morceaux de cinq bits, chaque morceau de cinq bits représentant l'occupation de la pièce orientée sur une rangée horizontale de la partie supérieure du plateau, la position initiale de la pièce orientée étant toujours dans le coin supérieur gauche. La fonction unfold_pieceWxH applique à la pièce les décalages appropriés qui lui font prendre toutes les autres positions envisageables.

Enfin on obtient la liste de toutes les positions possibles des cinq tétrominos non orientés à l'aide de ces cinq déclarations :

 
Sélectionnez
let tetro_i = List.flatten [tetro_i0;tetro_i1]
and tetro_l = List.flatten [tetro_l0;tetro_l1;tetro_l2;tetro_l3;tetro_j0;tetro_j1;tetro_j2;tetro_j3]
and tetro_o = List.flatten [tetro_oo]
and tetro_s = List.flatten [tetro_s0;tetro_s1;tetro_z0;tetro_z1]
and tetro_t = List.flatten [tetro_t0;tetro_t1;tetro_t2;tetro_t3]
;;

V-A-3. 31. La génération des coups

Souvenez-vous de la fonction rev_map_filter :

 
Sélectionnez
let rev_map_filter p f l =
  let rec loop l acc =
    match l with
    | [] -> acc
    | h::t ->
        if p h then loop t (f h::acc)
        else loop t acc
  in loop l [];;

Au lieu de lui donner la liste vide [] comme valeur initiale de l'accumulateur acc nous pourrions très bien lui donner une liste quelconque, de ce fait on réaliserait un append en plus de map et filter, ce qui nous donne une nouvelle version, rev_append_map_filter :

 
Sélectionnez
let rev_append_map_filter p f l1 l2 =
  let rec loop l acc =
    match l with
    | [] -> acc
    | h::t ->
        if p h then loop t (f h::acc)
        else loop t acc
  in loop l1 l2;;

C'est cette version que nous mettrons dans notre module ListXP.

Pour générer les coups légalement jouables, il nous faut définir une fonction du type :

 
Sélectionnez
bitboard -> bitboard list

Nous l'appellerons play_moves, elle démarre avec une liste vide [] et pour chacun des tétrominos I.L.O.S.T. elle augmente cette liste avec les nouveaux tableaux de jeux générés par la possession de ce tétromino en utilisant rev_append_map_filter :

 
Sélectionnez
let play_moves (b: bitboard ) =
  let hand1  = b.first  land hand_mask 
  and hand2  = b.second land hand_mask 
  and board1 = b.first  land board_mask 
  and board2 = b.second land board_mask
  in
  let can_move p =
    (p.upper land board1) lor 
    (p.lower land board2) = 0
  and play_move h p =
    {first  = p.upper + board1 + hand2;
     second = p.lower + board2 + hand1 - h}
  in let legal_moves h t acc = 
    if (hand1 land h) = 0 then acc
    else ListXP.rev_append_map_filter can_move (play_move h) t acc
  in begin 
    if (b.first land hand_mask) = 0 then
      []
    else
      let i = legal_moves hand_i tetro_i [] in
      let l = legal_moves hand_l tetro_l  i in
      let o = legal_moves hand_o tetro_o  l in
      let s = legal_moves hand_s tetro_s  o in
      let t = legal_moves hand_t tetro_t  s in
                                          t
  end;;

V-A-4. 32. Le calcul de la valeur d'un tableau

Voici simplement le type game qui prend les trois valeurs théoriques possibles pour un jeu de stratégie à information complète pour deux joueurs :

 
Sélectionnez
type game = First | Second | Tie;;

Nous définissons ainsi la valeur théorique d'un tableau de jeu :

  • si cinq coups par joueur ont été joués alors le résultat est Tie ;
  • sinon ce tableau peut générer une liste de tableaux par la fonction play_moves ;
  • si cette liste est vide alors le résultat est First ;
  • sinon si cette liste contient au moins un tableau dont la valeur est First alors le résultat est Second ;
  • sinon si cette liste contient au moins un tableau dont la valeur est Tie alors le résultat est Tie ;
  • sinon le résultat est First.

Nous écrivons maintenant la fonction eval qui donne la valeur théorique d'un tableau de jeu au nième coup suivant cette définition :

 
Sélectionnez
let rec eval (b: bitboard ) n =
  if n = 10 then Tie
  else eval_list (play_moves b) (succ n)
and eval_list l n =
  let rec loop l acc =
    if acc = Second then acc
    else match l with
    | [] -> acc
    | h::t ->
       ( match eval h n with
       | First  -> Second
       | Second -> loop t acc
       | Tie    -> loop t Tie )
  in loop l First;;

Ainsi que le code principal qui affiche la valeur théorique du jeu (la valeur théorique du tableau de jeu initial) :

 
Sélectionnez
print_string
  ( match eval {first = hand_mask; second = hand_mask} 0 with
  | First  ->  "First wins\n"
  | Second ->  "Second wins\n"
  | Tie    ->  "Tie game\n" );;

précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Damien Guichard. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.