20131125:TP:OCaml:Objets
Sommaire
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)
- (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.