Author: Christian.Queinnec@lip6.fr

## Exercices

### Coordonnées polaires

Les coordonnées de la bibliothèque `Graphics` sont cartésiennes. Un segment y est représenté par son point de départ (x0,y0) et son point d'arrivée (x1,y2) Il peut être utile d'utiliser des coordonnées polaires. Un segment est alors décrit par son point de départ (x0,y0) la taille du segment (r) et son angle (a) Le rapport entre coordonnées cartésiennes et coordonnées polaires est définie par les équations :

| ì í î |  |
x1 | = | x0 + r * cos(a)
y1 | = | y0 + r * sin(a)

Le type suivant définit les coordonnées polaires d'un segment :

    # type seg_pol = {x:float; y:float; r:float; a:float};;
    type seg_pol = { x: float; y: float; r: float; a: float }

1. Écrire la fonction `to_cart` de transformation de coordonnées polaires en coordonnées cartésiennes.

       # let to_cart p =
          (p.x,p.y),(p.x +. p.r *. (cos p.a), p.y +. p.r *. (sin p.a)) ;;
       val to_cart : seg_pol -> (float * float) * (float * float) = <fun>

2. Écrire la fonction `draw_seg` qui affiche un segment défini en coordonnées polaires dans le repère de `Graphics`.

       # let draw_seg p =
          let (x1,y1),(x2,y2) = to_cart p in
            Graphics.moveto (int_of_float x1) (int_of_float  y1);
            Graphics.lineto (int_of_float x2) (int_of_float y2) ;;
       val draw_seg : seg_pol -> unit = <fun>

3. Un des intérêts des coordonnées polaires est de pouvoir facilement appliquer des transformations sur un segment. Une translation ne modifiera que le point de départ, une rotation sera uniquement effectuée sur le champ angle, de même pour un changement d'échelle sur le champ longueur. De façon générale, on peut représenter une transformation comme un triplet de flottants : le premier représente la translation (on ne considère, ici, que des translations sur la droite du segment), le second la rotation et le troisième le facteur d'échelle. Définir la fonction `app_trans` qui prend un segment en coordonnées polaires et un triplet de transformations et retourne le nouveau segment.

       # let app_trans seg ( da, dr, dxy ) =
          let _,(x1,y1) = to_cart {seg with r = seg.r *. dxy} in
            {x=x1; y=y1; a=seg.a +. da; r=seg.r *. dr} ;;
       val app_trans : seg_pol -> float * float * float -> seg_pol = <fun>

4. On peut construire des dessins récursifs par itération de transformations. Écrire la fonction `dessin_r` qui prend en arguments un segment `s`, un nombre d'itérations `n`, une liste de transformations `l` et affiche tous les segments résultant des transformations sur `s` itérées jusqu'à `n`.

       # let rec dessin_r s n l =
          if n = 0 then ()
          else
          begin
            draw_seg s;
            List.iter (fun t -> dessin_r (app_trans s t) (n-1) l) l
          end ;;
       val dessin_r : seg_pol -> int -> (float * float * float) list -> unit = <fun>

5. Vérifier que le programme suivant produit bien les images de la figure 5.10.

       let pi = 3.1415927 ;;
       let s = {x=100.; y= 0.; a= pi /. 2.; r = 100.} ;;
       dessin_r s 6 [ (-.pi/.2.),0.6,1.; (pi/.2.), 0.6,1.0] ;;
       Graphics.clear_graph();;
       dessin_r s 6 [(-.pi /. 6.), 0.6, 0.766;
                     (-.pi /. 4.), 0.55, 0.333;
                     (pi /. 3.), 0.4, 0.5 ] ;;

       # Graphics.close_graph();;
       - : unit = ()
       # Graphics.open_graph ":0 200x200";;
       - : unit = ()
       # let pi = 3.1415927 ;;
       val pi : float = 3.1415927
       # let s = {x=100.; y= 0.; a= pi /. 2.; r = 100.} ;;
       val s : seg_pol = {x=100; y=0; r=100; a=1.57079635}
       # dessin_r s 6 [ (-.pi/.2.),0.6,1.; (pi/.2.), 0.6,1.0] ;;
       - : unit = ()
       # Graphics.clear_graph();;
       - : unit = ()
       # dessin_r s 6 [(-.pi /. 6.), 0.6, 0.766;
                      (-.pi /. 4.), 0.55, 0.333;
                      (pi /. 3.), 0.4, 0.5 ] ;;
       - : unit = ()

> Figure 5.10 : dessins récursifs

### Éditeur de bitmaps

On cherche à un écrire un petit éditeur de bitmap (à la manière de la commande `bitmap` de X-window). Pour cela on représente un bitmap par ses dimensions (largeur et hauteur), la taille des pixels, et un tableau à 2 dimensions de booléens.

1. Définir un type etat_bitmap décrivant l'information nécessaire pour conserver les valeurs des pixels, la taille du bitmap et les couleurs d'affichage et d'effacement.

       # type etat_bitmap  =
          {w : int; h : int; fg : Graphics.color; bg : Graphics.color;
           pix : bool array array; s : int} ;;
       type etat_bitmap =
         { w: int;
           h: int;
           fg: Graphics.color;
           bg: Graphics.color;
           pix: bool array array;
           s: int }

2. Écrire les fonctions de création d'un bitmap (`create_bitmap`) et d'affichage d'un bitmap (`draw_bitmap`) .

       # let create_bitmap x y f g t =
          let r = Array.make_matrix  x y false in
          { w = x; h = y; fg = f;  bg = g; pix = r; s = t} ;;
       val create_bitmap :
         int -> int -> Graphics.color -> Graphics.color -> int -> etat_bitmap =
         <fun>

       # let draw_pix i j s c  =
          Graphics.set_color c;
          Graphics.fill_rect (i*s+1) (j*s+1) (s-1) (s-1) ;;
       val draw_pix : int -> int -> int -> Graphics.color -> unit = <fun>

       # let draw_bitmap b =
          for i=0 to b.w-1 do
            for j=0 to b.h-1 do
               draw_pix i j b.s (if b.pix.(i).(j) then b.fg else b.bg)
            done
          done ;;
       val draw_bitmap : etat_bitmap -> unit = <fun>

3. Écrire les fonctions `read_bitmap` et `write_bitmap` qui respectivement lit et écrit un bitmap dans un fichier passé en paramètre en suivant le format ASCII de X-window. Si le fichier n'existe pas, alors la fonction de lecture crée un nouveau bitmap en utilisant la fonction `create_bitmap`.

       # let read_file filename =
          let ic = open_in filename in
            let rec aux  () =
              try
                let line =  (input_line ic) in
                line :: (aux ())
              with End_of_file ->  close_in ic ; []
          in aux ();;
       val read_file : string -> string list = <fun>

       # let read_bitmap filename  =
          let r = Array.of_list (read_file filename)  in
          let h = Array.length r in
          let w = String.length r.(0) in
          let b = create_bitmap w h Graphics.black Graphics.white 10 in
            for j = 0 to  h - 1 do
              for i = 0 to w - 1 do
                b.pix.(i).(j) <-  ( r.(j).[i] = '#')
              done
            done;
            b ;;
       val read_bitmap : string -> etat_bitmap = <fun>

       # let save_bitmap filename b =
          let oc = open_out filename in
          let f x = output_char oc (if x then '#' else '-')  in
          Array.iter (fun x -> (Array.iter f x); output_char oc '\n') b.pix ;
          close_out oc ;;
       val save_bitmap : string -> etat_bitmap -> unit = <fun>

   Un pixel allumé est représenté par le caractère `#`, l'absence d'un pixel par le caractère `-`. Chaque ligne de caractères représente une ligne du bitmap. On pourra tester le programme en utilisant les fonctions `atobm` et `bmtoa` de X-window qui effectueront les conversions entre ce format ASCII et le format des bitmaps créés par la commande `bitmap`. Voici un exemple.

        ###################-------------#######---------######
        ###################---------------###-------------##--
        ###-----###-----###---------------###-------------#---
        ##------###------##----------------###-----------##---
        #-------###-------#-----------------###---------##----
        #-------###-------#-----------------###--------##-----
        --------###--------------------------###-------#------
        --------###-------###############-----###----##-------
        --------###-------###---------###------###--##--------
        --------###-------###----------##-------###-#---------
        --------###-------###-----------#-------#####---------
        --------###-------###-----------#--------###----------
        --------###-------###--------------------####---------
        --------###-------###--------------------####---------
        --------###-------###------#-----------##---###-------
        --------###-------###------#----------##----###-------
        --------###-------##########----------#------###------
        --------###-------##########---------##-------###-----
        --------###-------###------#--------##--------###-----
        --------###-------###------#-------##----------###----
        --------###-------###--------------#------------###---
        ------#######-----###-----------#######--------#######
        ------------------###---------------------------------
        ------------------###-----------#---------------------
        ------------------###-----------#---------------------
        ------------------###----------##---------------------
        ------------------###---------###---------------------
        ------------------###############---------------------

4. On reprend le squelette de boucle d'interaction de la page ?? pour construire l'interface graphique de l'éditeur. L'interface homme-machine est fort simple. Le bitmap est affiché en permanence dans la fenêtre graphique. Un clic souris sur une des cases du bitmap inverse sa valeur. Ce changement est répercuté à l'écran. L'appui sur la touche 'S' sauve le bitmap dans le fichier. L'appui sur la touche 'Q' fait sortir du programme.
   - Écrire la fonction `start` de type etat_bitmap -> unit -> unit qui ouvre la fenêtre graphique et affiche le bitmap passé en paramètre.

         # exception Fin ;;
         exception Fin
         # let squel f_init f_end f_key f_mouse f_except =
            f_init ();
            try
              while true do
                try
                  let s = Graphics.wait_next_event
                            [Graphics.Button_down; Graphics.Key_pressed]  in
                  if s.Graphics.keypressed
                  then f_key s.Graphics.key
                  else if s.Graphics.button
                       then f_mouse s.Graphics.mouse_x s.Graphics.mouse_y
                with
                    Fin -> raise Fin
                  |  e  -> f_except e
              done
            with
              Fin -> f_end () ;;
         val squel :
           (unit -> 'a) ->
           (unit -> unit) ->
           (char -> unit) -> (int -> int -> unit) -> (exn -> unit) -> unit = <fun>

         # let start b () =
            let ow = 1+b.w*b.s and oh = 1+b.h*b.s in
            Graphics.open_graph (":0 " ^ (string_of_int ow) ^ "x" ^ (string_of_int oh)) ;
            Graphics.set_color (Graphics.rgb 150 150 150) ;
            Graphics.fill_rect 0 0 ow oh ;
            draw_bitmap b ;;
         val start : etat_bitmap -> unit -> unit = <fun>

   - Écrire la fonction `stop` qui ferme la fenêtre graphique et sort du programme.

         # let stop () = Graphics.close_graph() ; exit 0 ;;
         val stop : unit -> 'a = <fun>

   - Écrire la fonction `mouse` de type etat_bitmap -> int -> int -> unit qui modifie l'état du pixel correspondant au clic souris et affiche ce changement.

         # let mouse b x y  =
            let i,j = (x / b.s),(y/b.s) in
            if ( i < b.w ) && ( j < b.h) then
              begin
                b.pix.(i).(j) <- not b.pix.(i).(j) ;
                draw_pix i j b.s (if b.pix.(i).(j) then b.fg else b.bg)
              end ;;
         val mouse : etat_bitmap -> int -> int -> unit = <fun>

   - Écrire la fonction `key` de type string -> etat_bitmap -> char -> unit qui prend en paramètre un nom de fichier, un bitmap et le caractère de la touche appuyée et effectue les actions associées : sauvegarde dans un fichier pour la touche 'S' et déclenchement de l'exception `Fin` pour la touche 'Q'.

         # let key  filename b c =
            match c with
              'q' | 'Q' -> raise Fin
            | 's' | 'S' -> save_bitmap filename b
            | _ -> () ;;
         val key : string -> etat_bitmap -> char -> unit = <fun>

5. Écrire une fonction `go` qui prend en paramètre un nom de fichier, charge le bitmap puis l'affiche et lance la boucle d'interaction.

       # let go name =
          let b =  try
                     read_bitmap name
                   with
                      _ -> create_bitmap 10 10 Graphics.black Graphics.white 10
          in squel (start b) stop (key name b) (mouse b) (fun e -> ()) ;;
       val go : string -> unit = <fun>

### Ver de Terre

Le ver de terre est un petit organisme, longiforme, d'une certaine taille qui va croître soit avec le temps soit en ramassant des objets dans un monde. Le ver de terre est toujours en mouvement selon une direction. Ses actions propres ou dirigées par un joueur permettent uniquement un changement de direction. Le ver de terre disparaît s'il touche un bord du monde ou s'il passe sur une partie de son corps. On le représente le plus souvent par un vecteur de coordonnées avec deux indices principaux : sa tête et sa queue. Un mouvement sera donc le calcul des nouvelles coordonnées de sa tête, et son affichage, et l'effacement de sa queue. Une croissance ne modifiera que sa tête sans toucher à la queue du ver de terre.

1. Écrire le ou les types Objective CAML pour représenter un ver de terre et le monde où il évolue. On peut représenter un ver de terre par une file d'attente de ses coordonnées.

       # type cell = Vide | Pleine ;;
       type cell = | Vide | Pleine

       # type monde = { l : int; h : int; cases : cell array array } ;;
       type monde = { l: int; h: int; cases: cell array array }

       # type vdt = { mutable tete : int; mutable queue : int; mutable taille : int;
                     mutable vx : int; mutable vy : int;
                     pos : (int * int) array } ;;
       type vdt =
         { mutable tete: int;
           mutable queue: int;
           mutable taille: int;
           mutable vx: int;
           mutable vy: int;
           pos: (int * int) array }

       # type jeu = { m : monde; v : vdt; t_cell : int;
                     fg : Graphics.color; bg : Graphics.color } ;;
       type jeu =
         { m: monde;
           v: vdt;
           t_cell: int;
           fg: Graphics.color;
           bg: Graphics.color }

2. Écrire les fonctions d'initialisation et d'affichage d'un ver de terre dans un monde.

       # let init_monde  larg  haut   =
          { l = larg; h = haut; cases = Array.create_matrix larg haut Vide} ;;
       val init_monde : int -> int -> monde = <fun>

       # let init_vdt t t_max larg =
          if t > larg then failwith "init_vdt"
          else
            begin
              let v = { tete = t-1; queue = 0; taille = t;  vx = 1; vy = 0;
                        pos = Array.create t_max (0,0) } in
              let y  = larg / 2
              and rx = ref (larg/2 - t/2) in
              for i=0 to t-1 do v.pos.(i) <- (!rx,y) ; incr rx done ;
              v
            end ;;
       val init_vdt : int -> int -> int -> vdt = <fun>

       # let init_jeu t t_max larg haut tc c1 c2 =
          let j = { m = init_monde larg haut; v = init_vdt t t_max larg;
                    t_cell = tc;  fg = c1;   bg = c2 } in
          for i=j.v.tete to j.v.queue do
            let (x,y) =  j.v.pos.(i) in
            j.m.cases.(x).(y) <- Pleine
          done ;
          j ;;
       val init_jeu :
         int -> int -> int -> int -> int -> Graphics.color -> Graphics.color -> jeu =
         <fun>

       # let affiche_case x y t c =
          Graphics.set_color c;
          Graphics.fill_rect (x*t) (y*t) t t ;;
       val affiche_case : int -> int -> int -> Graphics.color -> unit = <fun>

       # let affiche_monde  jeu =
          let m = jeu.m in
          for i=0 to m.l-1 do
            for j=0 to m.h-1 do
              let col = if m.cases.(i).(j) = Pleine then jeu.fg else jeu.bg in
              affiche_case i j jeu.t_cell col
            done
          done ;;
       val affiche_monde : jeu -> unit = <fun>

3. Modifier la fonction `squel` de squelette du programme qui à chaque tour dans la boucle d'interaction effectue une action, paramétrée par une fonction. La gestion de l'événement clavier ne doit pas être bloquante.

       (**************  interaction **********)

       # let tempo  ti  =
          for i = 0 to ti do ignore (i * ti * ti ) done ;;
       val tempo : int -> unit = <fun>

       # exception Fin;;
       exception Fin
       # let squel f_init f_end f_key f_mouse f_except f_run =
          f_init ();
          try
            while true do
              try
                tempo 200000 ;
                if Graphics.key_pressed() then f_key (Graphics.read_key()) ;
                f_run ()
              with
                  Fin -> raise Fin
                |  e  -> f_except e
            done
          with
            Fin -> f_end () ;;
       val squel :
         (unit -> 'a) ->
         (unit -> unit) ->
         (char -> unit) -> 'b -> (exn -> 'c) -> (unit -> 'c) -> unit = <fun>

4. Écrire la fonction `run` qui fait avancer le ver de terre dans le jeu. Cette fonction déclenche les exceptions `Gagne` (si le ver a atteint une certaine taille) et `Perdu` s'il rencontre une case pleine ou un bord du monde.

       # exception Perdu;;
       exception Perdu
       # exception Gagne;;
       exception Gagne

       # let run jeu temps itemps () =
          incr temps;
          let v = jeu.v in
          let t = Array.length v.pos in
            let ox,oy = v.pos.(v.tete) in
            let nx,ny = ox+v.vx,oy+v.vy in
              if (nx < 0 ) || (nx >= jeu.m.l) || (ny < 0) || (ny >= jeu.m.h)
              then raise Perdu
              else if jeu.m.cases.(nx).(ny) = Pleine then raise Perdu
              else if v.tete = v.queue then raise Gagne
              else
                begin
                  let ntete = (v.tete + 1) mod t in
                  v.tete <- ntete ;
                  v.pos.(v.tete) <- (nx,ny);
                  jeu.m.cases.(nx).(ny) <- Pleine ;
                  affiche_case nx ny jeu.t_cell jeu.fg ;
                  if (!temps mod !itemps < (!itemps - 2)) then
                  begin
                    let qx,qy = v.pos.(v.queue) in
                    jeu.m.cases.(qx).(qy) <- Vide ;
                    affiche_case qx qy jeu.t_cell jeu.bg ;
                    v.queue <-  (v.queue + 1) mod t
                  end
                end ;;
       val run : jeu -> int ref -> int ref -> unit -> unit = <fun>

5. Écrire la fonction d'interaction clavier qui modifie la direction du ver de terre.

       # let key lact jeu  c =
          match c with
            'q' | 'Q' -> raise Fin
          | 'p' | 'P' -> ignore (Graphics.read_key ())
          | '2' | '4' | '6' | '8' ->
              let dx,dy = List.assoc c lact in
              jeu.v.vx <- dx ;
              jeu.v.vy <- dy
          | _ -> () ;;
       val key : (char * (int * int)) list -> jeu -> char -> unit = <fun>

6. Écrire les autres fonctions utilitaires de gestion de l'interaction et les passer au nouveau squelette de programme.

       # let start jeu () =
          let ow = jeu.t_cell * jeu.m.l and oh = jeu.t_cell * jeu.m.h  in
          let size = (string_of_int ow) ^ "x" ^ (string_of_int oh) in
          Graphics.open_graph (":0 " ^ size) ;
          Graphics.set_color  (Graphics.rgb 150 150 150);
          Graphics.fill_rect 0 0 ow oh;
          affiche_monde jeu;
          ignore (Graphics.read_key()) ;;
       val start : jeu -> unit -> unit = <fun>

       # let stop jeu  () =
          ignore (Graphics.read_key());
          Graphics.close_graph() ;;
       val stop : 'a -> unit -> unit = <fun>

       # let mouse  x y  = () ;;
       val mouse : 'a -> 'b -> unit = <fun>

       # let except e  =   match e with
            Perdu -> print_endline "PERDU"; raise Fin
          | Gagne -> print_endline "GAGNE"; raise Fin
          |  e -> raise e ;;
       val except : exn -> 'a = <fun>

7. Écrire la fonction principale de lancement de l'application.

       # let la = [ ('2',(0,-1)) ; ('4',(-1,0)) ; ('6',(1,0)) ; ('8',(0,1)) ] ;;
       val la : (char * (int * int)) list =
         ['2', (0, -1); '4', (-1, 0); '6', (1, 0); '8', (0, ...)]

       # let go larg haut tc  =
          let col = Graphics.rgb 150 150 150 in
          let jeu =  init_jeu 5 (larg*haut /2) larg haut tc Graphics.black col in
          squel (start jeu) (stop jeu) (key la jeu) mouse except (run jeu (ref 0) (ref 100));;
       val go : int -> int -> int -> unit = <fun>