20131125:TP:OCaml:Objets

De wiki-prog
Aller à : navigation, rechercher


Introduction

Le but de ce TP est de manipuler un certains nombre de notions objets vu en cours.

Nous allons mettre en place une forme de plateau avec des entités de couleurs différentes et des règles de déplacement et d’interaction automatiques pour voir ce qu'il se passe après.

Ce TP est prévu pour deux semaine avec rendu à la fin, certaines notions ont été vues en cours la semaine dernière (semaine du 18/11) et d'autres seront vues cette semaine (semaine du 25/11.) Profiter bien de la séance de cette semaine pour poser vos questions, car la semaine suivante (semaine du 2/12) les AOC seront en atelier et vous n'aurez pas de chargés de TP (vous pouvez quand même venir en salle machine.)

Le suffixe du rendu est tp20131125 et le rendu est ouvert à l'adresse: [1]

Votre archive login-tp20131125.tar.bz2 devra contenir

  • login-tp20131125
    • AUTHORS
    • _tags
    • ADD_ON
    • living.ml

Le fichier _tags contiendra:

<*>: use_graphics

Normalement, avec ce fichier, votre programme devrait compiler avec la commande:

$ ocamlbuild living.native

Le rendu est prévu pour la semaine des soutenances, je sais. Mais:

  • vous avez deux semaines pleines pour faire ce TP;
  • ce que vous ferez dans ce TP peut vous aider à finir votre projet et vous aidera dans vos révisions pour les partiels de décembre.

Entités

Les entités sont représentés par des objets dont la classe dérive d'une classe de base (entity) proposant proposant les méthodes de base (getter/setter, mouvement … )

  • Compléter le code suivant
class entity name start_x start_y col =
object (self)
  val mutable x:int = start_x
  val mutable y:int = start_y
  val mutable color = col
 
  (* return position in form of a pair of int *)
  method get_position = (* FIX ME *)
 
  (* set position to nx ny *)
  method move_to nx ny =
    begin
      (* FIX ME *)
    end
 
  (* move using a delta (use move_to) *)
  method move_by dx dy =
    self#move_to (x+dx) (y+dy)
 
  (* get and set color *)
  method get_color = (* FIX ME *)
  method set_color c = (* FIX ME *)
 
  (* get name *)
  method get_name = name
 
  (* return a string representing the entity *)
  method to_string =
    Printf.sprintf "%s[%s](%d,%d)" name color x y
 
end

Affichage text

Avant d'afficher nos entités de manière graphique, nous allons fournir des facilités d'affichage en utilisant la composition d'objet.

Voici une classe, print_helper, que vous devrez intégrer à la classe entity pour la rendre « imprimable ».

class ['obj] print_helper =
object (self)
  (* JEDI MODE: you don't see the next line *)
  val mutable obj:'obj = Obj.magic None
  method register o = obj <- o
  method print =
    Format.printf "@[<b 2>Entity:@;@[<h>%s@]@]@," obj#to_string
end
  • Compléter la définition suivante:
class printable_entity name start_x start_y col =
object (self)
 
  (* inherit from entity *)
  inherit (* FIX ME *)
 
  (* add a printer *)
  val printer = new print_helper
 
  (* Use printer to print *)
  method print = (* FIX ME *)
 
  initializer
    (* Register to the printer *)
    printer#register self
end

Le plateau

Le plateau est un simple tableau dont les cases contiennent soient rien, soient un objet de la famille entity.

Nous commencerons par définir les exceptions suivantes qui nous seront utiles lorsque les cases seront vide (ou ne seront pas vide pour certains cas.)

exception Cell_not_empty of int*int
exception Cell_empty of int*int
  • Compléter la définition suivante:
(* 'a: will be the type of element on the board *)
class ['a] board size_x size_y =
object (self)
  val tab = Array.make_matrix size_x size_y None
 
  (* put an entity at (x,y) *)
  (* raise Cell_not_empty if needed *)
  method put_entity x y (ent:'a) =
    begin
      (* FIX ME *)
    end
 
  (* return the entity at (x,y) *)
  (* raise Cell_empty if needed *)
  method get_entity x y =
    (* FIX ME *)
 
  (* move some entity from pos to pos *)
  method move_entity old_x old_y x y =
    begin
      self#put_entity x y (self#get_entity old_x old_y);
      (* Thanks to exception, we only get there if everything is fine *)
      tab.(old_x).(old_y) <- None;
    end
 
end

Voici l'extension des entités pour supporter le plateau:

class move_entity name start_x start_y col (board:'a) =
object (self:'self)
  inherit entity name start_x start_y col as ent
 
  constraint 'a = 'self board
 
  method move_to nx ny =
    begin
      board#move_entity x y nx ny;
      ent#move_to nx ny;
    end
 
  initializer
    board#put_entity start_x start_y self
 
end

Attaquer et défendre

Globalement, la vie des entités correspondra à trois activités: se déplacer, attaquer et défendre. Nous verrons la vraie nature des mouvements plus tard. Pour l'instant concentrons nous sur la notion d'attaque et de défense.

Chaque entité va être munie d'une force (strength) et d'une résistance (resistance), lorsqu'une entité arrive sur une case non vide, elle attaque celle se trouvant déjà dessus. C'est l'entité attaquée (celle qui défend) qui devra alors gérer la situation: si sa résistance est inférieure à la force de l'attaquante, alors elle prend la couleur de celle-ci. Dans tous les cas, l'attaquante revient à sa position initiale.

  • Compléter la classe suivante:
class ['ae] fight_entity name start_x start_y col board strength resistance =
object (self)
  inherit (* FIX ME *) as m_ent
 
  (* get strength and resistance *)
  method get_strength:int = (* FIX ME *)
  method get_resistance:int = (* FIX ME *)
 
  (* defense against ae, another-entity *)
  method defense (ae:'ae) =
    (* FIX ME *)
 
  (* atack *)
  method attack (ae:'ae) = (* FIX ME *)
 
  (* Override move_to *)
  method move_to nx ny =
    (* FIX ME *)
 
end

Gestion des tours

Acteurs

Pour gérer les tours nous allons implémenter une opération générique sur le plateau permettant d'agir sur toutes les entités en jeux. L'objectif est simple: on transmet un acteur qui dispose d'une méthode effectuant une action sur une entité et on appelle cet acteur sur chaque entité du plateau. Il nous font donc une méthode d'itération qui prend en paramètre un objet et applique la méthode treat de l'objet sur toutes les entités du plateau.

Notre problème: normalement, il n'y a pas une entité par case du plateau, et même il devrait y avoir bien plus de cases libres que de cases pleines. Du coup, traverser le plateau nous coûte cher pour rien. Par contre, comme les objets sont « uniques », on peut les stocker dans le plateau et ailleurs, comme dans une liste par exemple. Il nous faut donc étendre la méthode put_entity pour stocker les entités ajoutées dans une liste.

En résumé, nous allons étendre la classe board par composition (pour changer.) On commencera par déclarer une classe virtuelle pour fixer les types. On va modifier légèrement la classe board pour qu'elle hérite de cette classe. Ajouter la définition suivante avant la définition de la classe board:

class virtual ['a] virtual_board =
object
  method virtual put_entity: int -> int -> 'a -> unit
  method virtual get_entity: int -> int -> 'a
  method virtual move_entity: int -> int -> int -> int -> unit
end

Puis ajouter la ligne suivante au début de la classe board:

inherit ['a] virtual_board

Il faut aussi modifier la définition des move_entity:

class move_entity name start_x start_y col (board:'a) =
object (self:'self)
  inherit entity name start_x start_y col as ent
 
  constraint 'a = 'self #virtual_board
 
  method move_to nx ny =
    begin
      board#move_entity x y nx ny;
      ent#move_to nx ny;
    end
 
  initializer
    board#put_entity start_x start_y self
end
  • Compléter la classe iterable_board:
class ['a, 'b] iterable_board (board:'a virtual_board) =
object (self)
 
  inherit ['a] virtual_board
 
  (* Forwarded methods *)
  method get_entity x y = board#get_entity x y
  method move_entity old_x old_y x y = board#move_entity old_x old_y x y
 
  (* add some store for entities, like a list *)
  val mutable entities = (* FIX ME *)
 
  (* forward put_entity to the board
     and then (if nothing goes wrong)
     add it to the store
   *)
  method put_entity x y ent =
 
  (* iterate over each entity e and call actor#treat e *) 
  method iterate (actor:'b) = (* FIX ME *)
 
end

Nous allons voir maintenant à quoi peuvent ressembler les acteurs. Tout d'abord, le type virtuel:

class virtual ['a] abstract_actor =
object
  method virtual treat : 'a -> unit
end

Et maintenant, un acteur qui déplace chaque entité sur une position aléatoire:

class ['a] basic_random_move_actor range_x range_y =
object
  inherit ['a] abstract_actor
  method treat e =
    let (x,y) = (Random.int range_x, Random.int range_y) in
    e#move_to x y
end

Ce déplacement est un peu basique, on voudrait quelque chose de plus évolué. L'idée est la suivante: on impose une distance maximale de déplacement, pour chaque déplacement on tire une distance aléatoire bornée par la distance maximale, on tire aléatoirement une direction (plus ou moins), puis on calcule le mouvement en x et en y en utilisant un coefficient aléatoire (entre 0 et 1, en float donc) et un peu de géométrie (Pythagore … ), finalement tout ça nous permet de changer les coordonnées de l'entité. Voici un petit résumé de ce qu'il faut faire:

  • calcul de (dx,dy) en fonction de dmax:
    • sign = tirage aléatoire produisant -1 ou 1
    • dist = tirage aléatoire entre 0 et dmax
    • coef = tirage aléatoire entre 0 et 1
    • (cx,cy) = (coef, 1-coef)
    • d = \sqrt{\frac{\text{dist}^2}{cx^2 + dx^2}}
    • (dx,dy) = (sign * cx * d, sign * cy * d)
  • calcul de (nx,ny) en fonction (x,y) (position de l'entité):
    • nx = max(0,min(tailleX - 1, dx+x))
    • ny = max(0,min(tailleY - 1, dy+y))
  • Écrire la classe random_move_actor qui propose ce type de déplacement
class ['a] random_move_actor range_x range_y range_dist =
object (self)
  inherit ['a] abstract_actor
 
  (* FIX ME *)
 
end

Afin de pouvoir appliquer plusieurs acteurs sur chaque entité, voici un acteur particulier (que nous pourrons utiliser plus tard):

class ['a, 'b] chain_actor =
object
  inherit ['a] abstract_actor
 
  constraint 'b = 'a #abstract_actor
 
  val actors = Queue.create ()
 
  method register (a:'b) =
    Queue.push a actors
 
  method treat e =
    Queue.iter (fun a -> a#treat e) actors
end

Le Maître

Nous allons maintenant concevoir la classe abstract_master qui gère les tours du jeu.

exception The_End
 
class virtual ['a] abstract_master (turns:int) (board:'a) =
object (self)
 
  val mutable current = 0
 
  (* Actor management *)
  val actors = new chain_actor
  method register_actor a = actors#register a
 
  (* virtual display *)
  method virtual display : unit
  (* virtual init *)
  method virtual init_board : unit
 
  method next_turn =
    begin
      current <- current + 1;
      if current > turns then raise The_End;
      board#iterate actors;
      self#display
    end
 
end

Initialisation du plateau

Nous allons maintenant nous intéresser à l'initialisation du plateau: il nous faut des équipes (même couleur et même caractéristiques, nom unifiés … ) et un remplissage un peu aléatoire du plateau.

Équipes

Nous allons créer une classe dont le rôle est de fournir une couleur, un nom et des caractéristiques sur une base commune.

Une équipe est définie par: une couleur (string), un coefficient d'attaque (entre 0 et 100, le coefficient de défense étant le complément sur 100.)

Chaque équipe doit tenir le compte de ses membres et nome chaque nouveau membre en utilisant le code suivant (color est la couleur et members le nombre de membre): Printf.sprintf "%s%02d" color members

Les équipes produisent des objets de la classe fight_entity.

Les caractéristiques sont obtenues avec l'algo suivant:

  • d1 = tirage aléatoire entre 0 et 10
  • d2 = tirage aleatoire entre 0 et 10
  • base = d1 * 10 + d2
  • (attaque, defense) = (coef_att * base, coef_def * base)
  • Compléter la définition de classe suivante
class team board color attcoef =
object (self)
  val mutable members = 0
  val defcoef = 100 - attcoef
 
  (* Create a new entity *)
  method new_member x y = (* FIX ME *)
 
end

Population !

Nous allons maintenant étendre la classe abstract_master pour la remplir !

Nous allons définir trois équipes et les règles pour créer une entité sur une place donnée.

Les équipes seront:

  • red_team = new team board "red" 60
  • green_team = new team board "green" 50
  • blue_team = new team board "blue" 40

Les règles de création:

  • 20% de chance qu'une case soit une entité
  • Équiprobabilité entre les trois équipes
  • Compléter la classe:
class virtual ['a] team_master turns board size_x size_y =
object (self)
  inherit ['a] abstract_master turns board
 
  val red_team = (* FIX ME *)
  val blue_team = (* FIX ME *)
  val green_team = (* FIX ME *)
  val teams = Array.create 3 (Obj.magic None)
 
  initializer
    teams.(0) <- red_team;
    teams.(1) <- green_team;
    teams.(2) <- blue_team;
 
  method init_board =
    for x = 0 to size_x - 1 do
      for y = 0 to size_y - 1 do
        (* FIX ME *)
      done
    done
end

Affichage

Nous allons maintenant afficher un peu tout ça.

Affichage text

Faire un affichage texte n'est pas super compliqué. Nous voulons un résultat de ce genre:

---------------------
|X|G|X|X|X|X|X|X|B|X|
---------------------
|X|X|X|X|G|X|X|R|X|X|
---------------------
|X|X|X|R|X|X|R|X|X|X|
---------------------
|X|X|R|X|X|X|B|X|X|R|
---------------------
|X|X|G|X|X|X|X|X|X|X|
---------------------
|X|G|X|X|X|B|R|X|X|X|
---------------------
|X|B|X|X|X|X|X|X|X|X|
---------------------
|X|X|X|R|X|X|X|G|X|X|
---------------------
|X|X|X|X|B|R|R|X|X|X|
---------------------
|X|R|X|X|X|X|X|X|R|X|
---------------------
  • Compléter la classe suivante
class ['a] text_master turns board size_x size_y colors =
object (self)
  inherit ['a] team_master turns board size_x size_y
 
  val tab = Hashtbl.create 11
 
  initializer
    List.iter (fun (col,ch) -> Hashtbl.add tab col ch) colors
 
  (* Get the char for a color using Hashtbl.find tab color *)
  method display = (* FIX ME *)
 
end

Affichage graphique

Si vous voulez un rendu plus visuel, voici la classe pour l'affichage graphique:

class ['a] graphic_master turns board size_x size_y colors =
object (self)
  inherit ['a] team_master turns board size_x size_y
 
  val tab = Hashtbl.create 11
 
  initializer
    List.iter (fun (col,ch) -> Hashtbl.add tab col ch) colors;
    Graphics.open_graph (Printf.sprintf " %dx%d" size_x size_y);
 
  method display =
    begin
      Graphics.clear_graph ();
      for x = 0 to size_x - 1 do
        for y = 0 to size_y - 1 do
          try
            Graphics.set_color
              (Hashtbl.find tab ((board#get_entity x y)#get_color));
            Graphics.plot x y
          with _ -> ()
        done
      done;
      (* Wait for a Key *)
      ignore (Graphics.wait_next_event [Graphics.Key_pressed]);
    end
end

Rassembler le tout

Nous allons maintenant tout rassembler.

Il nous faut:

  • un plateau
  • un maître
  • une action (au moins) pour chaque tour

Voici un exemple avec le maître graphique:

let rec until_the_end master =
  try
    master#next_turn;
    until_the_end master
  with
      The_End -> ()
 
let main () =
begin
  let (dX, dY) = (200, 200) in
  let board = new iterable_board (new board dX dY) in
  let master = new graphic_master 10 board dX dY
    [("red",Graphics.red);("green",Graphics.green);("blue",Graphics.blue)] in
  master#register_actor (new random_move_actor dX dY 50);
  master#init_board;
  master#display;
  until_the_end master;
  exit 0
end
 
let _ = main ()

Et un avec le maître texte:

let rec until_the_end master =
  try
    master#next_turn;
    until_the_end master
  with
      The_End -> ()
 
let main () =
begin
  let (dX, dY) = (10, 10) in
  let board = new iterable_board (new board dX dY) in
  let master = new text_master 10 board dX dY
    [("red",'R');("green", 'G');("blue",'B')] in
  master#register_actor (new random_move_actor dX dY 5);
  master#init_board;
  master#display;
  until_the_end master;
  exit 0
end
 
let _ = main ()

Programme à rendre

Pour le rendu vous devez compléter l'ensemble des classes (bien sûre) et fournir un programme qui prendra en paramètre:

  • le choix du mode (graphique ou texte)
  • les dimensions du plateau
  • la distance maximale par mouvement

Vous pourrez également écrire d'autres acteurs et faire toutes les améliorations qui vous plaisent, celle-ci devront par contre être décrite dans un fichier ADD_ON fourni avec votre rendu.