mai
2010

Bonjour,
Il y a quelques semaines, j’ai découvert les échecs (quelques astuces de base du moins), et j’ai vite été tenté d’essayer de faire un programme informatique qui pourrait calculer dans une position donnée le meilleur coup à jouer, et donc jouer aux échecs. Ce qui au départ devait être le projet d’une soirée ou deux c’est vite retrouvé un projet plus important. Dans ce billet, je vais essayer de vous décrire les étapes et les choix que j’ai réalisé dans la création de ce moteur d’échec.

Fonctionnement d’un programme de jeux d’échecs.
L’idée principale qui se cache derrière tous les moteurs d’échecs, c’est de jouer virtuellement tous les coups possibles et de prendre celui qui nous semble le meilleur.
Par exemple, si vous avez 3 coups A, B et C, alors on commence par jouer le coup A, et on joue ensuite pour l’adversaire. Si l’adversaire peut jouer les coups A’ et B’, alors on lui fait jouer le coup A’, puis on regarde les coups que l’on peut jouer, etc., et ceci jusqu’à une profondeur donnée. Le but va être de maximiser notre score final en choisissant, dans notre exemple, le coup A, B, ou C qui semble être le meilleur. On joue pour l’adversaire de la même façon que l’on jouerait pour nous-même : on le fait maximiser son score.
C’est l’algorithme min-max, qui consiste à maximiser nos gains en supposant que l’adversaire va minimiser ses pertes.
Par exemple, prenons l’arbre des coups de la partie suivante.

Nous sommes à la position A, et nous pouvons jouer les coups B1, B2 ou B3.
Si on joue B1, soit l’adversaire joue C1 et perd 12 points, soit il joue C2 et perd 10 points, soit il joue C3 et perd 3 points.
On considère donc que l’adversaire va jouer C3 et on évalue donc le coup B1 à 3 points.
Si on joue B2, la branche est alors évaluée à 5 points (l’adversaire joue C4).
Si on joue B3, la branche est évaluée à 2 points (l’adversaire joue C8).
Au final, le meilleur coup se révèle être B2, puisqu’il permet d’obtenir au minimum 5 points.
Voilà pour le principe de base. Il existe par la suite quelques optimisations qui permettent par exemple de ne pas explorer tous les cas.
Pour réaliser cet algorithme nous avons donc besoin d’une fonction qui donne à un instant donné tous les coups que l’on peut jouer selon les règles des échecs, une fonction pour jouer et pour annuler un coup, et une fonction pour évaluer une position.
Les règles des échecs
La première étape consiste donc à représenter le plateau des échecs. J’ai utilisé pour cela un tableau en deux dimensions, bien qu’il existe des solutions plus performantes (comme les bitboard).
J’utilise une classe Board qui a été réalisée par un ami, cgizmo. C’est une classe générique à tous les jeux de plateaux rectangulaires, qui contient toutes les fonctions pour manipuler un tableau à deux dimensions, mais aussi une fonction pour afficher un plateau, et une fonction pour annuler un coup (rollback).
Quand je veux jouer un coup, je fais plateau#start_move(); je manipule mon plateau, puis je termine le coup avec plateau#end_move().
Ainsi, pour annuler le coup, je fais simplement plateau#rollback() et la classe annule toutes les modifications de mon plateau qui ont été faites entre les deux marqueurs.
J’ai ensuite créé une classe Chess, qui utilise la classe Board, mais qui est spécifique aux échecs.
Elle est capable de savoir si un coup est valide, de retourner tous les coups valides, de savoir si on est échec et mat, pat, etc.
Cette classe est assez importante en taille : les règles des échecs sont en fait assez compliqués.
Par exemple :
-Les pions avance de une case normalement, sauf s’ils peuvent manger une pièce adverse (ils peuvent se déplacer alors sur les côtés) ou s’ils sont sur leur première ligne (ils peuvent avancer de deux), ou encore même s’ils se trouvent sur la 5ième ligne et que l’adversaire avance un pion de deux cases (prise en passant).
-On a le droit de roquer une fois par partie s’il n’y a aucune pièce entre le roi et la tour, et si aucune des cases entre le roi et la tour n’est pas menacée, etc.
Bref, je n’ai pas implémenté toutes les règles d’un coup, j’ai fait cela par étapes.
Pour tester que tout allait bien, j’ai simplement fait jouer les deux joueurs en prenant à chaque tour un coup au hasard parmi les coups valides. La partie doit finir soit par un pat, soit par un mat !
Il y a également dans cette classe la fonction d’évaluation qui accorde un score à une position d’une partie. Par exemple, elle peut compter les pièces restantes sur le plateau, regarder la position des pièces (les pièces aux centres sont valorisés par ex), etc.
La mienne est très simple, et c’est un point qu’il faudrait améliorer car la fonction d’évaluation est très importante dans un moteur d’échec.
Pour finir, voici la signature de ma classe Chess :
object
val mutable board : field board
val mutable castling_b : bool * int
val mutable castling_w : bool * int
val mutable king_b : int * int
val mutable king_w : int * int
val mutable moves : dep list
val mutable piece_captured : bool
val mutable turn : color
method private add_move : dep -> unit
method apply : field array array -> unit
method private available_move :
int * int -> int * int -> piece_type -> bool -> bool * dep option
method board : field board
method private can_castling : bool
method cancel : unit
method capture : bool
method castling : color -> bool * int
method private check_castling : int * int -> int * int -> bool
method private check_enpassant : int * int -> int * int -> bool
method check_move :
int * int -> int * int -> piece_type -> bool -> bool * dep option
method edit_castling : color -> bool -> bool -> unit
method edit_king : color -> int * int -> unit
method edit_turn : unit
method eval : color -> ExtendN.score
method private get_all : bool -> bool -> dep list
method get_moves : bool -> dep list
method init : unit
method is_check : int * int -> bool
method king : color -> int * int
method private mouvements_p :
int * int -> bool -> ((int * int) * piece_type) list
method move_piece : dep -> unit
method moves : dep list
method print : unit
method private printc : int * int -> unit
method turn : color
end
L’intelligence artificielle
J’ai donc commencé par écrire l’algorithme min-max, qui m’a permis de voir 3 demis-coups à l’avance, ce qui est assez ridicule : mais quelle joie de voir mon truc tenter une variante du coup de berger :p !
Je me suis tourné ensuite vers les coupures alpha/bêta. C’est une amélioration de min/max qui repose sur cette constatation : supposons que vous pouvez jouer 3 coups, et que le premier coup vous rapporte 3 points. Vous explorez alors le deuxième coup et vous voyez que l’adversaire peut jouer un coup qui va ne vous rapporter que 2 points. Comme on sait que vous pouvez gagner 3 points en jouant le premier coup, cela ne sert à rien de continuer sur le deuxième coup, il sera forcément moins bon que le premier ! On peut donc couper cette brancher et explorer directement le troisième coup.
Voici une implémentation de cet algorithme :
let rec loop best al bt = function
| [] ->
let sb, cb = best in
(* Si on a perdu dans tous les cas *)
if sb = MInf then
let lm = game#get_moves true in
if lm <> [] then (MInf, List.hd lm)
else
if game#is_check (game#king (game#turn)) then best
else
(* Si il y a pat *)
(N 0, cb)
else
best
| (s, mvt)::tail ->
game#move_piece mvt;
let score =
if prof = 0 || s = MInf || s = PInf then s
else let s, _ = alphabeta game ((--) bt) ((--) al) false (prof-1) in (--) s
in
game#cancel;
let n_best = if score >>= (fst best) then (score, mvt) else best in
let n_alpha = if fst n_best >>= al then fst n_best else al in
if n_alpha >> bt then n_best
else
loop n_best n_alpha bt tail
in
(* On récupère et on trie les coups possibles *)
let l = game#get_moves ck in
let nl = List.map (fun mvt ->
game#move_piece mvt;
let s = game#eval !!(game#turn) in game#cancel; (s, mvt)
) l in
let l' = List.sort (fun (a, _) (b, _) -> compare b a) nl in
loop (MInf, Dep((0,0), (0,0))) alpha beta l'
Pour plus d’information sur cet algorithme, je vous recommande cette page qui est assez bien faite :
http://jeudechecs.ifrance.com/pageechecs.htm
J’arrivais à voir grâce à cet algorithme jusqu’à 5 demi-coups à l’avance. Je commençais à battre un bon débutant :).
J’ai essayé pleins de modifications de cet algo. J’ai essayé de faire une version qui au lieu de me renvoyer le meilleur coup présumé, me renvoi l’arbre entier des coups, ainsi qu’une fonction prolonge_coup qui pourrait me prolonger l’arbre jusqu’à une profondeur donnée.
Ainsi par exemple, je pourrais créer l’arbre sur une profondeur de 4 demi-coups, sélectionner les 5 meilleurs coups à cette profondeur, puis continuer avec ces 5 coups jusqu’à 6 demis-coups par exemple.
La difficulté dans la fonction prolonge_coups est de bien faire remonter les valeurs de alpha et de bêta dans l’arbre pour produire toutes les coupures de la même façon que l’algorithme alpha/bêta.
Voici l’implémentation de tout cela.
| B of tree list
| Node of feuille * tree list
| Leaf of feuille
| End of score
;;
[...]
let rec alphabeta game alpha beta prof =
let rec loop best al bt l = function
| [] ->
if best = MInf then
let lm = game#get_moves true in
if lm <> [] then ([Leaf(MInf, (List.hd lm))], MInf)
else
if game#is_check (game#king (game#turn)) then ([End MInf], MInf)
else
(* Si il y a pat *)
([End (N 0)], N 0)
else
(l, best)
| (s, mvt)::tail ->
game#move_piece mvt;
let tree, s =
if prof <= 0 || s = PInf then (Leaf(s, mvt), s)
else
let r, s = alphabeta game ((--) bt) ((--) al) (prof -1) in
let s = (--) s in
(Node ((s, mvt), r), s)
in
game#cancel;
if s = PInf then ([tree], PInf)
else
let n_best = if s >> best then s else best in
let n_alpha = if n_best >>= al then n_best else al in
(*
Si il y a une coupure, on renvoit le coup qui a produit la coupure,
suivit des autres que l'on a déjà calculé ou que l'on ne calcule pas.
Il seront necessaire pour prolonger l'arbre
*)
if n_alpha >> bt then (tree::l@(l_to_tree tail), n_best)
else loop n_best n_alpha bt (if s >> MInf then insert tree l else l) tail
in
let l = game#get_moves false in
let nl= List.map (fun mvt ->
game#move_piece mvt;
let s = game#eval !!(game#turn) in game#cancel; (s, mvt)
) l in
let l' = List.sort (fun (a, _) (b, _) -> compare b a) nl in
let r, s = (loop MInf alpha beta [] l') in
(r, s)
;;
[..]
let rec prolonge_tree game alpha beta n tree =
let rec loop best al bt l = function
| [] ->
(l, best)
| t::tail ->
let nt = prolonge_tree game ((--)bt) ((--)al) (n-1) t in
let s = get_score_of_tree nt in
let n_best = if s >>= best then s else best in
let n_alpha = if n_best >>= al then n_best else al in
if n_alpha >> bt then (nt::l@tail, n_best)
else loop (if s >>= best then s else best) n_alpha bt (insert nt l) tail
in
match tree with
| Node((s, d), l) ->
game#move_piece d;
let r, score = loop MInf alpha beta [] l in
game#cancel;
Node(((--)score, d), r)
| Leaf(s, d) ->
game#move_piece d;
let r, s = alphabeta game alpha beta n in
game#cancel;
Node(((--) s, d), r)
| End score -> End score
| B l ->
let nl, _ = loop MInf alpha beta [] l in
B nl
;;
Cette version me permet donc de faire une sélection des coups, ce qui améliore un peu la performance du moteur (mais rien d’extraordinaire).
Quelques fioritures
J’ai rajouté un dictionnaire d’ouvertures (j’utilise les ouvertures de gnuchess) pour être meilleur en début de partie, ainsi qu’un module xboard pour jouer avec l’interface graphique xboard.
Conclusion
Bien que les performances de mon moteur soient plutôt médiocre, je garde un bon souvenir de ce projet. C’était la première fois que j’essayais de faire une intelligence artificielle. Les échecs ne sont pas franchement un très bon choix pour un premier jet, mais finalement je m’en suis pas trop mal tiré.
Le code source, pas très intéressant, et dont l’organisation en classes est franchement discutable est disponible ici :
http://code.google.com/p/epicchess/
J’espère que ce retour sur ce petit projet vous a plu,
Merci d’avance pour tout vos commentaires, améliorations, questions, etc. !
Cordialement,
Quentin

Un article de quentin_
Merci pour ton commentaire :).
En fait j’avais déjà vu ce lien, mais je n’avais pas vu que l’auteur a publié la suite des articles. C’est dommage, ça aurait pu me servir :p.
Indiscutablement un très beau projet
Une bonne ressource sur les techniques IA-Chess :
http://www.gamedev.net/reference/programming/features/chess1/