Précédent Index Suivant

Des robots en goguette

L'exemple de ce paragraphe illustre l'utilisation d'objets ainsi que la bibliothèque graphique. Nous verrons comment Objective CAML  reprend les notions d'héritage simple, d'héritage multiple, de redéfinition de méthode et de liaison dynamique. Nous verrons également comment les classes paramétrées peuvent être mises à profit.

L'application comprend deux catégories principales d'objets : un monde et des robots. Le monde est un ensemble de cases sur lesquelles évoluent des robots. Nous aurons plusieures classes de robots. Chacune d'elles possédera sa propre stratégie de déplacement dans le monde. Le principe d'interaction du monde et des robots est ici extrêmement simple. Le monde est entièrement maître du jeu : il demande tour à tour à chacun des robots qu'il connaît quelle est sa prochaine position. Chaque robot détermine sa prochaine position à l'aveugle : il ne connaît ni la géométrie du monde, ni les autres robots présents. Si la position demandée par un robot est légale et libre alors le monde l'y déplace.

Le monde matérialisera l'évolution des robots par une interface. La complexité (toute relative) de la conception et du développement de cet exemple est dans la toujours nécessaire séparation entre un traitement (ici : l'évolution des robots) et son interface (ici : la trace de cette évolution).

Description générale.
L'application est développée en deux temps.
  1. un ensemble de définitions donnant des classes de calcul pur pour le monde et pour divers robots envisagés.
  2. un ensemble de définitions, utilisant les précédentes et ajoutant ce qui est nécessaire à la mise en place d'une interface. Nous donnerons deux exemples d'interfaces : une rudimentaire sous forme de texte ; une, plus élaborée, utilisant la bibliothèque graphique.

Robots << éthérés >>

Dans un premier temps, nous nous intéressons aux robots hors de toute considération sur l'environnement qui les entourent, c'est à dire de l'interface qui les affiche.

# class virtual robot (i0:int) (j0:int) = 
object
val mutable i = i0
val mutable j = j0
method get_pos = (i,j)
method set_pos (i', j') = i <- i'; j <- j'
method virtual next_pos : unit -> (int * int)
end ;;


En toute généralité, un robot est une entité connaissant, ou croyant connaître, sa position (i et j), capable de la donner à qui la lui demande (get_pos), susceptible de modifier cette connaissance si on la lui précise (set_pos) et sachant décider d'un éventuel mouvement vers une nouvelle position (next_pos).




Figure 17.9 : hiérarchie de classes des robots purs


Pour améliorer la lisibilité du programme, nous définissons les mouvements relatifs à une direction absolue :

# type dir = North | East | South | West | Nothing ;;
# let walk (x,y) = function
North -> (x,y+1) | South -> (x,y-1)
| West -> (x-1,y) | East -> (x+1,y)
| Nothing -> (x,y) ;;
val walk : int * int -> dir -> int * int = <fun>
# let turn_right = function
North -> East | East -> South | South -> West | West -> North | x -> x ;;
val turn_right : dir -> dir = <fun>


Du schéma induit par la classe virtuelle des robots, nous définissons quatre espèces de robots distinctes (voir la figure 17.9) en précisant leur manière de se déplacer : Le cas du robot interactif est différent des autres car son comportement est lié à l'interface qui permettra de lui communiquer des ordres. En attendant, nous nous appuyons sur une méthode qui, virtuellement, communique cet ordre et en conséquence la classe interactive_robot demeure abstraite.

Remarquons que non seulement les quatre classes de robots spécialisés héritent de la classe robot mais que de surcroît elles en ont le type. En effet, les seules méthodes que nous ayons ajoutées sont des méthodes privées et donc n'apparaissent pas dans le type des instances de ces classes (voir page ??). Ce point nous est indispensable si nous souhaitons considérer tous les robots comme des objets de même type.

Monde pur

Un monde pur est un monde indépendant de l'interface. Il y est connu l'ensemble des positions qu'un robot est susceptible d'occuper. Cela prend la forme d'une grille de taille l×h, d'une méthode is_legal assurant qu'un couple d'entiers est bien une position dans le monde, et d'une méthode is_free indiquant si un robot occupe ou non une position donnée.

En outre, un monde dispose de la liste des robots présents sur sa surface robots ainsi que d'une méthode add permettant de faire rentrer de nouveaux robots.

Pour finir, un monde est pourvu de la méthode run lui permettant de prendre vie.

# class virtual ['robot_type] world (l0:int) (h0:int) =
object(self)
val l = l0
val h = h0
val mutable robots = ( [] : 'robot_type list )
method add r = robots <- r::robots
method is_free p = List.for_all (fun r -> r#get_pos <> p) robots
method virtual is_legal : (int * int) -> bool

method private run_robot r =
let p = r#next_pos ()
in if (self#is_legal p) & (self#is_free p) then r#set_pos p

method run () =
while true do List.iter (function r -> self#run_robot r) robots done
end ;;
class virtual ['a] world :
int ->
int ->
object
constraint 'a =
< get_pos : int * int; next_pos : unit -> int * int;
set_pos : int * int -> unit; .. >
val h : int
val l : int
val mutable robots : 'a list
method add : 'a -> unit
method is_free : int * int -> bool
method virtual is_legal : int * int -> bool
method run : unit -> unit
method private run_robot : 'a -> unit
end


Le système de type d'Objective CAML ne permet pas de laisser le type des robots non déterminé (voir page ??). Pour résoudre ce problème, nous avions la possibilité de restreindre ce type à celui de la classe robot. Mais dans ce cas, nous nous interdisions de pouvoir peupler un monde d'autres objets que ceux ayant exactement le même type que robot. Donc, nous avons choisi de paramétrer la classe world par le type des robots qui le peuplent. Nous pourrons ensuite instancier ce paramètre de type par des robots textuels ou par des robots graphiques.

Robots textuels

Des objets texte
Pour obtenir des robots gérables par une interface texte, nous définissons la classe des objets textuels (txt_object).

# class txt_object (s0:string) =
object
val name = s0
method get_name = name
end ;;


Une classe de spécification : les robots textuels abstraits
Par héritage double de robots et txt_object, nous obtenons la classe abstraite txt_robot des robots textuels.

# class virtual txt_robot i0 j0 =
object
inherit robot i0 j0
inherit txt_object "Anonymous"
end ;;
class virtual txt_robot :
int ->
int ->
object
val mutable i : int
val mutable j : int
val name : string
method get_name : string
method get_pos : int * int
method virtual next_pos : unit -> int * int
method set_pos : int * int -> unit
end


Cette classe nous sert pour définir un monde à interface texte (voir page ??). Les habitants de ce monde ne seront ni des objets de txt_robot (puisque cette classe est abstraite) ni des héritiers de cette classe. La classe txt_robots est en quelque sorte une classe de spécification permettant au compilateur d'identifier les types des méthodes (calcul et interface) des habitants du monde à interface texte. L'utilisation d'une telle classe de spécification vient de la séparation que nous voulons maintenir entre les calculs et l'interface.

Les robots concrets en mode texte
Ils s'obtiennent simplement par double héritage; la figure 17.10 donne leur hiérarchie de classes.




Figure 17.10 : hiérarchie de classes des robots en mode texte


# class fix_txt_robot i0 j0 =
object
inherit fix_robot i0 j0
inherit txt_object "Fix robot"
end ;;

# class crazy_txt_robot i0 j0 =
object
inherit crazy_robot i0 j0
inherit txt_object "Crazy robot"
end ;;

# class obstinate_txt_robot i0 j0 =
object
inherit obstinate_robot i0 j0
inherit txt_object "Obstinate robot"
end ;;


Les robots interactifs doivent pour devenir concrets définir leur méthode d'interaction avec l'utilisateur.

# class interactive_txt_robot i0 j0 =
object
inherit interactive_robot i0 j0
inherit txt_object "Interactive robot"
method private get_move () =
print_string "Which dir : (n)orth (e)ast (s)outh (w)est ? ";
match read_line() with
"n" -> North | "s" -> South
| "e" -> East | "w" -> West
| _ -> Nothing
end ;;


Monde textuel

Le monde à interface texte se dérive du monde pur par
  1. héritage de la classe générique world en instanciant son paramètre de type par la classe de spécification txt_robot,
  2. redéfinition de la méthode run pour y inclure les différents affichages textuels.

# class virtual txt_world (l0:int) (h0:int) =
object(self)
inherit [txt_robot] world l0 h0 as super

method private display_robot_pos r =
let (i,j) = r#get_pos in Printf.printf "(%d,%d)" i j

method private run_robot r =
let p = r#next_pos ()
in if (self#is_legal p) & (self#is_free p)
then
begin
Printf.printf "%s is moving from " r#get_name ;
self#display_robot_pos r ;
print_string " to " ;
r#set_pos p;
self#display_robot_pos r ;
end
else
begin
Printf.printf "%s is staying at " r#get_name ;
self#display_robot_pos r
end ;
print_newline () ;
print_string"next - ";
ignore (read_line())

method run () =
let print_robot r =
Printf.printf "%s is at " r#get_name ;
self#display_robot_pos r ;
print_newline ()
in
print_string "Initial state :\n";
List.iter print_robot robots;
print_string "Running :\n";
super#run() (* 1 *)
end ;;


Nous attirons l'attention du lecteur sur l'appel à la méthode run de la classe ancêtre (marqué (* 1 *) dans le code) dans la redéfinition de cette même méthode. Nous avons là une illustration des deux types de liaison des méthodes possibles : statique ou dynamique (voir page ??). L'appel à super#run est statique; c'est l'intérêt de nommer la superclasse que de pouvoir appeler ses méthodes alors qu'elles ont été redéfinies. Par contre, dans cette méthode super#run se trouve un appel à self#run_robot. C'est ici une liaison dynamique qui a lieu; c'est la méthode définie dans la classe txt_world qui est exécutée et non celle de world, sans quoi nous n'obtiendrions aucun affichage.

Le monde plan rectangulaire textuel
s'obtient en implantant la dernière méthode encore abstraite : is_legal.

# class closed_txt_world l0 h0 =
object(self)
inherit txt_world l0 h0
method is_legal (i,j) = (0<=i) & (i<l) & (0<=j) & (j<h)
end ;;





Figure 17.11 : hiérarchie de classes du monde plan rectangulaire en mode texte


On peut procéder à un petit essai en tapant :

let w = new closed_txt_world 5 5
and r1 = new fix_txt_robot 3 3
and r2 = new crazy_txt_robot 2 2
and r3 = new obstinate_txt_robot 1 1
and r4 = new interactive_txt_robot 0 0
in w#add r1; w#add r2; w#add r3; w#add r4; w#run () ;;


Nous allons passer à présent à la réalisation de l'interface graphique pour notre monde de robots. En fin de course, nous obtiendrons une application ayant l'apparence de la figure 17.12.




Figure 17.12 : Le monde graphique des robots


Robots graphiques

Nous obtenons des robots en mode graphique en suivant le même schéma que le mode texte :
  1. définition d'un objet graphique générique,
  2. définition d'une classe abstraite de robots graphiques par double héritage des robots et des objets graphiques (analogue de la classe de spécification du paragraphe 17),
  3. définition par double héritage des robots possédant un comportement particulier.

Objets graphiques génériques

Un objet graphique simple est un objet possédant une méthode display qui prend en argument les coordonnées d'un pixel et s'affiche.

# class virtual graph_object =
object
method virtual display : int -> int -> unit
end ;;


De cette spécification, il est possible de tirer des objets graphiques extrêmement complexes. Nous allons nous contenter ici d'une classe graph_item affichant le bitmap qui sert à la construire.

# class graph_item x y im = 
object (self)
val size_box_x = x
val size_box_y = y
val bitmap = im
val mutable last = None

method private erase = match last with
Some (x,y,img) -> Graphics.draw_image img x y
| None -> ()

method private draw i j = Graphics.draw_image bitmap i j
method private keep i j =
last <- Some (i,j,Graphics.get_image i j size_box_x size_box_y) ;

method display i j = match last with
Some (x,y,img) -> if x<>i || y<>j
then ( self#erase ; self#keep i j ; self#draw i j )
| None -> ( self#keep i j ; self#draw i j )
end ;;


Un objet graph_item conserve la portion d'image sur laquelle il est affiché pour la restaurer lors de l'affichage suivant. De plus, si l'image n'a pas bougé elle n'est pas réaffichée.

# let foo_bitmap =  [|[| Graphics.black |]|] ;;
# class square_item x col =
object
inherit graph_item x x (Graphics.make_image foo_bitmap)
method private draw i j = Graphics.set_color col ;
Graphics.fill_rect (i+1) (j+1) (x-2) (x-2)
end ;;

# class disk_item r col =
object
inherit graph_item (2*r) (2*r) (Graphics.make_image foo_bitmap)
method private draw i j = Graphics.set_color col ;
Graphics.fill_circle (i+r) (j+r) (r-2)
end ;;

# class file_bitmap_item name =
let ch = open_in name
in let x = Marshal.from_channel ch
in let y = Marshal.from_channel ch
in let im = Marshal.from_channel ch
in let () = close_in ch
in object
inherit graph_item x y (Graphics.make_image im)
end ;;


Nous avons spécialisé les graph_item en carrés, disques et bitmaps lus depuis un fichier.

Le robot graphique abstrait
est à la fois un robot et un objet graphique.

# class virtual graph_robot i0 j0 =
object
inherit robot i0 j0
inherit graph_object
end ;;


Les robots graphiques fixes, fous et obstinés
sont des objets graphiques spécialisés.

# class fix_graph_robot i0 j0 =
object
inherit fix_robot i0 j0
inherit disk_item 7 Graphics.green
end ;;

# class crazy_graph_robot i0 j0 =
object
inherit crazy_robot i0 j0
inherit file_bitmap_item "crazy_bitmap"
end ;;

# class obstinate_graph_robot i0 j0 =
object
inherit obstinate_robot i0 j0
inherit square_item 15 Graphics.black
end ;;


Le robot graphique interactif
utilise les primitives key_pressed et read_key du module Graphics pour l'acquisition du déplacement. On reconnaîtra les touches 8, 6, 2 et 4 du pavé numérique (touche NumLock active). De cette façon, l'utilisateur n'est pas obligé de donner une indication de déplacement à chaque interrogation du monde.

# class interactive_graph_robot i0 j0 =
object
inherit interactive_robot i0 j0
inherit file_bitmap_item "interactive_bitmap"
method private get_move () =
if not (Graphics.key_pressed ()) then Nothing
else match Graphics.read_key() with
'8' -> North | '2' -> South | '4' -> West | '6' -> East | _ -> Nothing
end ;;


Monde graphique

On obtient un monde à interface graphique par héritage du monde pur en instanciant le paramètre 'a_robot avec la classe abstraite des robots graphiques graph_robot. Comme pour le monde en mode texte, le monde graphique redéfinit la méthode run_robot de traitement d'un robot et la méthode d'activation générale run.

# let delay x = let t = Sys.time () in while (Sys.time ()) -. t < x do () done ;;

# class virtual graph_world l0 h0 =
object(self)
inherit [graph_robot] world l0 h0 as super
initializer
let gl = (l+2)*15 and gh = (h+2)*15 and lw=7 and cw=7
in Graphics.open_graph (" "^(string_of_int gl)^"x"^(string_of_int gh)) ;
Graphics.set_color (Graphics.rgb 170 170 170) ;
Graphics.fill_rect 0 lw gl lw ;
Graphics.fill_rect (gl-2*lw) 0 lw gh ;
Graphics.fill_rect 0 (gh-2*cw) gl cw ;
Graphics.fill_rect lw 0 lw gh

method run_robot r = let p = r#next_pos ()
in delay 0.001 ;
if (self#is_legal p) & (self#is_free p)
then ( r#set_pos p ; self#display_robot r)

method display_robot r = let (i,j) = r#get_pos
in r#display (i*15+15) (j*15+15)

method run() = List.iter self#display_robot robots ;
super#run()
end ;;


Notez que la fenêtre graphique est créée à l'initialisation d'un objet de cette classe.

Le monde plan rectangulaire et graphique
s'obtient de la même manière que pour le monde plan rectangulaire et textuel.

# class closed_graph_world l0 h0 =
object(self)
inherit graph_world l0 h0
method is_legal (i,j) = (0<=i) & (i<l) & (0<=j) & (j<h)
end ;;
class closed_graph_world :
int ->
int ->
object
val h : int
val l : int
val mutable robots : graph_robot list
method add : graph_robot -> unit
method display_robot : graph_robot -> unit
method is_free : int * int -> bool
method is_legal : int * int -> bool
method run : unit -> unit
method run_robot : graph_robot -> unit
end


On peut alors tester l'application graphique en tapant

let w = new closed_graph_world 10 10 ;;
w#add (new fix_graph_robot 3 3) ;;
w#add (new crazy_graph_robot 2 2) ;;
w#add (new obstinate_graph_robot 1 1) ;;
w#add (new interactive_graph_robot 5 5) ;;
w#run () ;;


Pour en faire plus

L'implantation de la méthode run_robot des différents mondes sous-entend que les robots sont potentiellement capables de se rendre en tout point du monde du moment que celui-ci est libre et légal. De plus, rien n'interdit à un robot de modifier sa position sans en prévenir le monde. Une amélioration possible consiste à faire gérer l'ensemble des positions des robots par le monde; lors du déplacement d'un robot, le monde vérifie d'une part si la nouvelle position est légale mais aussi si elle constitue un déplacement autorisé. Dans ce cas, le robot devra être capable de demander au monde sa propre position; ce qui entraîne que la classe des robots devra être dépendante de la classe du monde. On pourra définir une classe robot prenant comme paramètre de type une classe de monde.

Cette modification permet alors de définir des robots capables d'interroger le monde qui les entoure et donc de se comporter en fonction de celui-ci. Nous pourrons réaliser des robots qui suivent ou qui fuient d'autres robots, qui tentent de les bloquer, etc. L'étape suivante est de permettre aux robots de communiquer entre eux pour s'échanger des informations et constituer ainsi des équipes de robots.

Les chapitres de la partie suivante de l'ouvrage permettent de libérer l'exécution des robots les unes des autres : soit en ayant recours aux Threads (voir page ??) pour que chacun s'exécute sur un processus distinct, soit en profitant des possibilités de l'informatique distribuée (voir page ??) pour que les robots soient des clients s'exécutant sur des machines distantes qui annoncent leur déplacement ou demandent des informations à un monde qui serait un serveur. Ce problème est traité à la page ??.








Précédent Index Suivant