Quelques raccourcis :

420-KBB-LG – Programmation orientée objet avancée

Ceci est un petit site de support pour le cours 420-KBB-LG – Programmation orientée objet avancée.

Vous trouverez aussi des liens sur divers langages (dont C#, notre outil de prédilection dans ce cours) un peu partout dans http://h-deb.ca/. Portez une attention particulière à ../../../Sujets/Divers--cdiese/index.html.

Les diverses sections de cette page (en fonction desquelles vous trouverez quelques liens dans l'encadré à droite) vous mèneront elles-aussi sur des pistes qui vous permettront d'explorer un peu plus par vous-mêmes, de valider vos acquis et d'enrichir votre apprentissage.

Cliquez sur cette cible pour le plan de cours, sous forme électronique

Pratiques de correction

Je corrige les programmes en appliquant des codes de correction. Vous trouverez ici la liste des codes les plus fréquents.

Ma stratégie de correction en tant que telle (pour le code, à tout le moins) est résumée ici.

Cliquez sur cette cible pour les normes appliquées dans ce cours, en ce qui a trait au pseudocode

Quelques trucs pour demander de l'aide plus efficacement

Détail des séances en classe

Puisque nous serons en quelque sorte laboratoire à la fois pour les séances théoriques et les séances de laboratoire, j'ai fait le choix de construire le cours sous forme de 30 séances (de S00 à S29, ou plutôt à S28 car les jours fériés introduisent du chaos dans ma planification) plutôt que sous forme de 15 séances théoriques et 15 séances de laboratoire. Le dosage prévu de temps en théorie et de temps en laboratoire (soit environ moitié-moitié) devrait être respecté.

Date Séance Détails

25 et 26 août

S00

Au menu :

  • Présentation du cours et du plan de cours
  • Les outils dont nous aurons besoin :
    • Nous utiliserons C# 12.0
    • Nous ferons des projets .NET 8
    • Assurez-vous que votre version de Visual Studio soit à jour!
  • Échange sur le contenu du cours, les modalités, les attentes
    • Il est possible que nous tenions une séance « en ligne » pour « roder la mécanique » au cas où la pandémie mettrait du sable dans l'engrenage (personne ne le souhaite, mais mieux vaut être prudentes et prudents)
  • Réponses aux questions de la classe :
  • Présentation d'une petite activité formative : 420KBB--Consignes-activite-revision.pdf

Si vous souhaitez le code du programme principal à partir duquel vous devrez démarrer, vous pouvez le prendre du fichier PDF ou encore le prendre ci-dessous (parfois, copier / coller d'un PDF...) :

// ...
List<Orque> orques = new List<Orque>();
try
{
   for(string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch(NomInvalideException ex)
{
   Console.WriteLine(ex.Message);
}
if(Trier(ref orques, out int nbPermutations))
{
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
}
else
{
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
}
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

Après avoir pris un peu de temps pour se « chamailler » avec ce petit défi de remise en forme, je vous ai proposé un peu de code pour vous aider à redémarrer vos instincts de programmeuse et de programmeur. L'accent a été mis sur l'écriture de code simple :

  • Écrire des fonctions
  • Viser « une vocation par fonction »
  • Essayer d'écrire des fonctions qui se limitent à une instruction quand cela s'avère possible
  • ... et se récompenser quand on y parvient, en se donnant le droit d'utiliser la notation => qui est concise et élégante

Le code produit en classe suit :

Program.cs
//
// code produit pour vous aider ce matin et pour amorcer une réflexion avec vous
//
using MonNamespace;
List<Orque> orques = new List<Orque>();
try
{
   for (string s = Console.ReadLine(); "" != s; s = Console.ReadLine())
   {
      orques.Add(new Orque(s));
      Console.WriteLine($"Orque créé : {orques[orques.Count - 1].Nom}");
   }
}
catch (NomInvalideException ex)
{
   Console.WriteLine(ex.Message);
}
if (Trier(ref orques, out int nbPermutations))
{
   Console.WriteLine("Les orques ont été entrés en ordre alphabétique");
}
else
{
   Console.WriteLine($"Trier les orques a nécessité {nbPermutations} permutations");
}
Console.Write("La tribu d'orques est :");
foreach (Orque orque in orques)
   Console.Write($" {orque.Nom}");

//
// la fonction qui suit est bien trop compliquée; faudra la revisiter...
//
static bool Trier(ref List<Orque> orques, out int nbPermutations)
{
   nbPermutations = 0;
   Orque[] orq = orques.ToArray(); 
   for (int i = 0; i < orq.Length - 1; ++i)
      for (int j = i + 1; j < orq.Length; ++j)
         if (orq[i].Nom.CompareTo(orq[j].Nom) > 0) // désordre
         {
            Permuter(ref orq[i], ref orq[j]);
            ++nbPermutations;
         }
   orques = orq.ToList();
   return nbPermutations == 0;
}
OutilsTexte.cs
// using...
static class OutilsTexte
{
   static char [] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
   public static bool Contient(char [] tab, char c)
   {
      foreach (char ch in tab)
         if (ch == c)
            return true;
      return false;
   }
   public static bool EstVoyelle(char c) =>
      Contient(voyelles, char.ToLower(c));
   public static int CompterVoyelles(string s)
   {
      int n = 0;
      foreach (char c in s)
         if (EstVoyelle(c))
            ++n;
      return n;
   }
}
Algos.cs
// using...
static class Algos
{
   public static bool EstEntreInclusif(int val, int min, int max) =>
      min <= val && val <= max;
}
Orque.cs
using static MonNamespace.Algos;
using static MonNamespace.OutilsTexte;
// ...
class NomInvalideException : Exception { }

class Orque
{
   const int LG_NOM_MIN = 1,
             LG_NOM_MAX = 4;
   static bool EstNomValide(string nom) =>
      EstEntreInclusif(nom.Length, LG_NOM_MIN, LG_NOM_MAX) &&
      OutilsTexte.CompterVoyelles(nom) <= 1;

   string nom;
   public string Nom
   {
      get => nom;
      private init
      {
            nom = EstNomValide(value) ?
               value : throw new NomInvalideException();
            //if (!EstNomValide(value))
            //   throw new NomInvalideException();
            //nom = value;
      }
   }
   public Orque(string nom)
   {
      Nom = nom;
   }
}

28 et 29 août

S01

Au menu :

  • Quelques mots sur l'idée de classe static, qui permet de pallier en partie à un manque de certains langages comme C# ou Java
    • Léger allègement syntaxique rendu possible par ce mécanisme
  • Retour sur la petite activité formative proposée à S00
  • Discussion de divers aspects techniques et architecturaux associés à cette activité
  • Avenues de raffinement ou d'optimisation
  • Quelques explorations qui nous mèneront vers notre premier travail pratique, le TP00

À titre de référence, le code d'aujourd'hui est à peu près le suivant. Ce code est perfectible; nous ferons bien mieux plus tard dans la session. Je vous laisse le soin d'écrire le programme principal :

Soldat.cs
// ...
class NomInvalideException : Exception { }
abstract class Soldat
{
   public string Nom { get; private init; }
   public Soldat(string nom)
   {
      Nom = nom;
   }
   public abstract void Saluer();
}
Orque.cs
// ...
class Orque : Soldat
{
   const int LG_NOM_MIN = 1,
             LG_NOM_MAX = 4;
   static bool EstNomValide(string s) =>
      EstEntreInclusif(s.Length, LG_NOM_MIN, LG_NOM_MAX) &&
      CompterVoyelles(s) <= 1;
   static string ValiderNom(string s) =>
      EstNomValide(s) ? s : throw new NomInvalideException();
      public Orque(string nom)
         : base(ValiderNom(nom))
      {
      }
      public override void Saluer()
      {
         Console.WriteLine($"MOI {Nom}, MOI PUE");
      }
   }
}

En espérant que cela vous soit utile!

Nous avons aussi discuté sommairement du clonage.

À titre de référence, le code produit lors de cette séance pour démontrer le clonage était :

Image[] images = new Image[]
{
   new Jpeg(ConsoleColor.Magenta),
   new Png(ConsoleColor.Green),
   new Bmp(ConsoleColor.Blue)
};
// non, pas le droit!
//foreach (Image img in images)
//{
//   img.Dessiner();
//   img = ModifierPeutÊtre(img); // <-- ceci serait illégal
//   img.Dessiner();
//}

for (int i = 0; i != images.Length; ++i)
{
   images[i].Dessiner();
   images[i] = ModifierPeutÊtre(images[i]);
   images[i].Dessiner();
}


////////////////////////

static Image ModifierPeutÊtre(Image img)
{
   // 0 : créer un backup
   Image backup = img.Cloner();

   // 1 : modifier img
   img.Teinte = ConsoleColor.Red;

   // 2 : demander si on veut conserver les modifs
   Console.WriteLine("Conserver les modifs? ");
   // 2a : si oui, on retourne img
   // 2b : sinon, on retourne le backup
   if (Console.ReadKey(true).Key == ConsoleKey.O)
      return img;
   return backup;
}


// il existe une interface ICloneable, qui expose une méthode Clone
// ... mais ne l'utilisez pas :) Pour des détails, voir :
// https://codeql.github.com/codeql-query-help/csharp/cs-class-implements-icloneable/
abstract class Image
{
   public ConsoleColor Teinte { get; set; }
   public Image(ConsoleColor teinte)
   {
      Teinte = teinte;
   }
   // Idiome NVI : non-virtual interface
   public void Dessiner()
   {
      ConsoleColor pre = Console.ForegroundColor;
      Console.ForegroundColor = Teinte;
      DessinerImpl(); // varie selon les enfants
      Console.ForegroundColor = pre;
   }
   protected abstract void DessinerImpl();
   public abstract Image Cloner();
}
class Jpeg : Image
{
   public Jpeg(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Jpeg {Teinte}");
   }
   protected Jpeg(Jpeg autre) : base(autre.Teinte)
   {
   }
   // spécialisation covariante
   public override Jpeg Cloner() => new (this);
}
class Bmp : Image
{
   public Bmp(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Bmp {Teinte}");
   }
   protected Bmp(Bmp autre) : base(autre.Teinte)
   {
   }
   public override Bmp Cloner() => new (this);
}
class Png : Image
{
   public Png(ConsoleColor teinte) : base(teinte)
   {
   }
   protected override void DessinerImpl()
   {
      Console.WriteLine($"Png {Teinte}");
   }
   protected Png(Png autre) : base(autre.Teinte)
   {
   }
   public override Png Cloner() => new (this);
}

En fin de séance, j'ai fait une petite activité dirigée d'un système à deux composants (code client, qui était une application console, et code serveur, qui était une bibliothèque de classes – une DLL).

Le code auquel nous en sommes arrivés pour le client était le suivant (j'ai pris quelques libertés pour vous divertir) :

using Arsenal;
FabriqueArmes fab = new ();
IArme p = fab.CréerArme(Arsenal.Gravité.violent);
p.Frapper();
p = fab.CréerArme(Arsenal.Gravité.délicat);
p.Frapper();

Le code auquel nous en sommes arrivés pour le serveur était le suivant (encore une fois avec quelques libertés) : 

namespace Arsenal
{
   public interface IArme
   {
      void Frapper();
   }
   class Masse : IArme
   {
      public void Frapper()
      {
         Console.WriteLine("POURRRRH");
      }
   }
   class Chainsaw : IArme
   {
      public void Frapper()
      {
         Console.WriteLine("FVRRRRRRRR!");
      }
   }
   public enum Gravité { violent, délicat }
   public class FabriqueArmes
   {
      public IArme CréerArme(Gravité grav) =>
         grav == Gravité.délicat ? new Masse() : new Chainsaw();
   }
}

Nous avons ensuite survolé les consignes du TP00.

Si votre serveur pour le TP00 fonctionne correctement, le programme de test suivant...

// ... code de test (note : le namespace se nomme Consommateur)
using GénérateurId;
using static Consommateur.Tests;

var fab = new FabriqueGénérateurs();
Test(fab, "Séquentiel", "ID", TypeGénérateur.Séquentiel);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Aléatoire", "ID", TypeGénérateur.Aléatoire, 3);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé, 3);
Test(fab, "Recycleur", "ID", TypeGénérateur.Recycleur);
Test(fab, "Partagé", "ID", TypeGénérateur.Partagé);
foreach (var (clé, valeur) in fab.ObtenirStatistiques())
   Console.WriteLine($"{clé} a été instancié {valeur} fois");

// ... placer ce qui suit dans une classe «static» nommée Tests
public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type)
{
   IGénérateurId p = fab.Créer(type, préfixe);
   var lst = new List<Identifiant>();
   Console.Write($"{nom}, pige initiale :\n\t");
   for (int i = 0; i != 10; ++i)
   {
      lst.Add(p.Prendre());
      Console.Write($"{lst[lst.Count - 1]} ");
   }
   Console.WriteLine();
   foreach (var n in lst)
      p.Rendre(n);
   Console.Write($"{nom}, pige post-remise :\n\t");
   for (int i = 0; i != 10; ++i)
      Console.Write($"{p.Prendre()} ");
   Console.WriteLine();
}

public static void Test(FabriqueGénérateurs fab, string nom, string préfixe, TypeGénérateur type, int germe)
{
   IGénérateurId p = fab.Créer(type, préfixe, germe);
   var lst = new List<Identifiant>();
   Console.Write($"{nom}, pige initiale :\n\t");
   for (int i = 0; i != 10; ++i)
   {
      lst.Add(p.Prendre());
      Console.Write($"{lst[lst.Count - 1]} ");
   }
   Console.WriteLine();
   foreach (var n in lst)
      p.Rendre(n);
   Console.Write($"{nom}, pige post-remise :\n\t");
   for (int i = 0; i != 10; ++i)
      Console.Write($"{p.Prendre()} ");
   Console.WriteLine();
}
// ...

... devrait donner un affichage comme le suivant (il peut y avoir certaines différences dans les cas « partagé » et « aléatoire », mais il y a des limites à ces différences – le test utilise un germe choisi – alors consultez votre chic prof si vous avez des doutes) :

Séquentiel, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Séquentiel, pige post-remise :
        ID00010 ID00011 ID00012 ID00013 ID00014 ID00015 ID00016 ID00017 ID00018 ID00019
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Aléatoire, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Aléatoire, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Partagé, pige initiale :
        ID19236 ID45716 ID56688 ID13007 ID36732 ID11833 ID16397 ID62078 ID22853 ID24902
Partagé, pige post-remise :
        ID32911 ID53056 ID45554 ID01984 ID05379 ID59249 ID08091 ID56063 ID49708 ID31213
Recycleur, pige initiale :
        ID00000 ID00001 ID00002 ID00003 ID00004 ID00005 ID00006 ID00007 ID00008 ID00009
Recycleur, pige post-remise :
        ID00009 ID00008 ID00007 ID00006 ID00005 ID00004 ID00003 ID00002 ID00001 ID00000
Partagé, pige initiale :
        ID14127 ID38283 ID32702 ID05286 ID65150 ID29877 ID33647 ID35818 ID24609 ID32588
Partagé, pige post-remise :
        ID19316 ID15965 ID33013 ID04178 ID45250 ID58540 ID33793 ID27893 ID56175 ID43324
Séquentiel a été instancié 1 fois
Recycleur a été instancié 2 fois
Aléatoire a été instancié 1 fois
Partagé a été instancié 1 fois

1 sept.

s/o

Fête du travail (jour férié)

2 sept.

?

La Fête du travail crée du chaos dans mon horaire alors on se limitera à travailler sur le TP00 pour éviter que la synchronisation ne se brise entre mes trois groupes.

4 et 5 sept.

S02

Au menu :

  • À venir

8 et 9 sept.

S03

Au menu :

  • À venir

11 et 12 sept.

S04

Au menu :

  • À venir

15 et 16 sept.

S05

Au menu :

  • À venir

18 et 19 sept.

S06

Au menu :

  • À venir

22 et 23 sept.

S07

Au menu :

  • À venir

25 et 26 sept.

S08

Au menu :

  • À venir

29 et 30 sept.

S09

Au menu :

  • À venir

2 et 3 oct.

S10

Au menu :

  • À venir

6 et 7 oct.

S11

Au menu :

  • À venir

9 et 10 oct.

s/o

Journée pédagogique (9 oct.) et journées de mise à niveau (10 oct.). Cours suspendus

13 oct.

s/o

Action de grâce (jour férié)

14 oct.

s/o

Journée de mise à niveau

16 et 17 oct.

S12

Au menu :

  • À venir

20 et 21 oct.

S13

Au menu :

  • À venir

23 et 24 oct.

S14

Au menu :

  • À venir

27 et 28 oct.

S15

Au menu :

  • À venir

30 et 31 oct.

S16

Au menu :

  • À venir

Joyeuse Halloween!

Halloween

3 et 4 nov.

S17

Au menu :

  • À venir

6 et 7 nov.

S18

Au menu :

  • À venir

10 et 11 nov.

S19

Au menu :

  • À venir

13 et 14 nov.

S20

Au menu :

  • À venir

17 et 18 nov.

S21

Au menu :

  • À venir

20 et 21 nov.

S22

Au menu :

  • À venir

24 et 25 nov.

S23

Au menu :

  • À venir

27 et 28 nov.

S24

Au menu :

  • À venir

1 et 2 déc.

S25

Au menu :

  • À venir

4 et 5 déc.

S26

Au menu :

  • À venir

8 et 9 déc.

S27

Au menu :

  • On fait la PFI (soyez prêtes, soyez prêts!)

10 et 11 déc.

S28

Au menu :

  • Chic examen final

Note : techniquement, le dernier jour de classe est le lundi 15 décembre, mais dû à l'ajustement fait pour compenser le désordre causé par la Fête du travail, je ne tiendrai pas de séance en classe ce jour-là. Je serai toutefois disponible pour vous (modalités à définir entre vous et moi)

Petits coups de pouces

Vous trouverez ici quelques documents, la plupart petits, qui peuvent vous donner un petit coup de pouce occasionnel.

Comment accéder à du code .NET à partir d'un client C++ natif.

Vous trouverez aussi des exemples de code C# dans la section Divers – C# du site, mais notez que je n'ai pas nécessairement harmonisé ces exemples (écrits pour des cours plus avancés, sous forme de survols) aux standards de programmation appliqués dans le présent cours. À lire avec prudence et discrimination, donc.

Consignes des travaux pratiques

Les consignes des travaux pratiques suivent.

Consignes Détails supplémentaires À remettre...

Activité de révision

Voir S00 et S01

s/o

TP00

Code de test proposé à la séance S01

Vendredi le 6 septembre 2024 à 23 h 59

Important : la copie électronique livrée par Colnet le 6 septembre est votre version finale, mais je ne corrigerai que la version papier que vous me remettrez au début du cours suivant (selon les groupes : le 10 ou le 11 septembre)

TP01

Vendredi le 11 octobre à 23 h 59

Un exemple d'exécution serait wallyd.mp4 (merci à Marc Beaulne d'avoir préparé cela!)

Le code du programme principal imposé est le suivant (vous pouvez ajouter des using et c'est tout) :

// ...
// remplir le catalogue
const char SYMBOLE_MÉTAL = '%';
Catalogue.Get.Associer(SYMBOLE_MÉTAL, Catégorie.Métal);
// préparer la surface d'affichage
const int HAUTEUR = 10,
          LARGEUR = 20;
Surface surf = new(HAUTEUR, LARGEUR);
// préparer le cadre de la surface
Cadre cadre = new(HAUTEUR, LARGEUR, ConsoleColor.Cyan);

// préparer la zone de messagerie
Messagerie messagerie = new(new(0, HAUTEUR));

// positionner Wallyd
Point2D centre = new(3, 3);
Robot wallyd = new("Wallyd", centre);
wallyd.Équiper(new DétecteurMétal(wallyd, 1.0f));
surf.Ajouter(wallyd);

// placer les déchets sur la surface
const int NB_DÉCHETS = 3;
Random dé = new();
List<Déchet> déchets = new();
var libres = surf.TrouverSi
(
   cadre.Exclure,
   c => c == default || c == ' '
);
if (libres.Count < NB_DÉCHETS)
   throw new SurfacePleineException();
for (int i = 0; i != NB_DÉCHETS; ++i)
{
   int n = dé.Next(libres.Count);
   déchets.Add(new(SYMBOLE_MÉTAL, Catégorie.Métal, libres[n]));
   libres.RemoveAt(n);
}
surf.Ajouter(déchets.ToArray());

while (surf.TrouverSi(c => c == SYMBOLE_MÉTAL).Count > 0)
{
   bool trouvé = false;
   do
   {
      PipelineAffichage pipeline = new();
      pipeline.Ajouter(Appliquer(cadre));
      pipeline.Appliquer
      (
         GénérerHalo(wallyd.Zone, surf.Dupliquer())
      );
      var pts = wallyd.Détecter(surf, Catégorie.Métal);
      if (pts.Count > 0)
      {
         trouvé = true;
         if (pts[0] == wallyd.Pos)
         {
            messagerie.Effacer();
            messagerie.Afficher
            (
               $"Déchet collecté à la position {pts[0]}"
            );
         }
         else
         {
            messagerie.Afficher
            (
               $"Trouvé {pts.Count} déchet(s)",
               $"Déplacement vers {pts[0]}"
            );
            surf.Retirer(wallyd);
            wallyd.DéplacerVers(pts[0]);
            surf.Ajouter(wallyd);
            wallyd.RéinitialiserPuissance();
         }
      }
      else
      {
         wallyd.AugmenterPuissance();
         messagerie.Effacer();
      }
      Thread.Sleep(500);
   }
   while (!trouvé);
   wallyd.RéinitialiserPuissance();
}
{
   PipelineAffichage pipeline = new();
   pipeline.Ajouter(Appliquer(cadre));
   pipeline.Appliquer
   (
      GénérerHalo(wallyd.Zone, surf.Dupliquer())
   );
}

static Mutable GénérerHalo(Cercle c, Mutable p)
{
   Mutable res = p.Dupliquer();
   for (int ligne = 0; ligne != res.Hauteur; ++ligne)
      for (int col = 0; col != res.Largeur; ++col)
      {
         Point2D pt = new(col, ligne);
         if (c.Centre.Distance(pt) <= c.Rayon)
            res[pt] = new(res[pt].Symbole, res[pt].Avant, ConsoleColor.Green);
      }
   return res;
}

static Func<Mutable, Mutable> Appliquer(Cadre p)
{
   return m =>
   {
      Mutable res = m.Dupliquer();
      for (int i = 0; i != p.Hauteur; ++i)
         for (int j = 0; j != p.Largeur; ++j)
         {
            var c = p[i, j];
            if (c.Symbole != default)
               res[i, j] = c;
         }
      return res;
   };
}
static Func<Mutable, Mutable> AppliquerSurface(Surface surf)
{
   return m =>
   {
      Mutable res = m.Dupliquer();
      for (int i = 0; i != surf.Hauteur; ++i)
         for (int j = 0; j != surf.Largeur; ++j)
         {
            var c = surf[i, j];
            if (c.Symbole != default)
               res[i, j] = c;
         }
      return res;
   };
}

class SurfacePleineException : Exception { }

Le code des fonctions fournies par vos chics profs pour la classe Surface est :

// ...
      public List<Point2D> TrouverSi
      (
         Func<Point2D, bool> exclure,
         Func<char, bool> pred
      )
      {
         List<Point2D> pts = new();
         for (int ligne = 0; ligne != Hauteur; ++ligne)
            for (int col = 0; col != Largeur; ++col)
            {
               Point2D pt = new(col, ligne);
               if (!exclure(pt) && pred(this[pt].Symbole))
                  pts.Add(pt);
            }
         return pts;
      }
      public List<Point2D> TrouverSi(Func<char, bool> pred) =>
         TrouverSi(pt => false, pred);
// ...

Le code fourni pour la classe PipelineAffichage est :

// ...
   internal class PipelineAffichage
   {
      List<Func<Mutable, Mutable>> Transfos = new();
      public void Ajouter(Func<Mutable, Mutable> transfo) =>
         Transfos.Add(transfo);
      public IProjetable Appliquer(Point2D pos, Mutable p)
      {
         foreach (var transfo in Transfos)
            p = transfo(p);
         return Afficher(pos, p);
      }
      public IProjetable Appliquer(Mutable p) =>
         Appliquer(new(), p);
// ...

TP02

Vendredi le 22 novembre à 23 h 59

Note : l'énoncé du TP02 est essentiellement correct, mais il vous est offert « sous réserve » car il se peut qu'il y ait des changements mineurs (surtout de la clarification) au cours des prochains jours.

Le code du programme principal imposé est le suivant (vous pouvez ajouter des using et c'est tout) :

// ...
RobotInfo[] robotsInfo =
{
   new(null, 0, "Wallyd", ConsoleColor.Magenta, Catégorie.Métal),
   new(null, 1, "Bart", ConsoleColor.Blue, Catégorie.Plastique),
   new(null, 2, "Charles", ConsoleColor.Yellow, Catégorie.Organique),
   new(null, 3, "Lisa", ConsoleColor.Green, Catégorie.Nucléaire)
};

(char sym, Catégorie cat)[] symCatégorie =
{
    ('%', Catégorie.Métal),
    ('*', Catégorie.Plastique),
    ('&', Catégorie.Plastique),
    ('/', Catégorie.Organique),
    ('?', Catégorie.Nucléaire),
};
foreach (var symCat in symCatégorie)
    Catalogue.Get.Associer(symCat.sym, symCat.cat);

// préparer la surface d'affichage
const int HAUTEUR = 20,
          LARGEUR = 40;
Surface surf = new(HAUTEUR, LARGEUR);
// préparer le cadre de la surface
Cadre cadre = new(HAUTEUR, LARGEUR, ConsoleColor.Cyan);

// préparer la zone de messagerie
Messagerie messagerie = new(new(LARGEUR, 0), HAUTEUR);
// préparer la zone d'informations
Messagerie information = new(new(0, HAUTEUR), 2);


Random dé = new();
List<(Robot r, Catégorie c)> robots =
   CréerRobots(robotsInfo, dé, surf, cadre.Exclure);

// placer les déchets sur la surface
const int NB_DÉCHETS = 3;
List<Déchet> déchets =
   CréerPollution(symCatégorie, dé, NB_DÉCHETS, surf, cadre.Exclure);
surf.Ajouter(déchets.ToArray());

CanalComm canal = new(robotsInfo.Length);

PipelineAffichage pipe = new (surf, cadre, canal, messagerie, information);

pipe.Démarrer();

CancellationTokenSource src = new();
Thread[] cueilleurs =
   CréerCueilleurs
   (
      robotsInfo, surf, canal, messagerie, information, src.Token
   );
Thread lireTouche = new(() =>
{
   Console.ReadKey(true);
   src.Cancel();
});
lireTouche.Start();
foreach (var c in cueilleurs) c.Start();
foreach (var c in cueilleurs) c.Join();
lireTouche.Join();
pipe.Arrêter();

static List<(Robot robot, Catégorie cat)>
   CréerRobots(RobotInfo [] robotsInfo, Random dé, Surface surf, Func<Point2D, bool> exclure)
{
   List<(Robot r, Catégorie c)> robots = new();
   // Trouver les cases libres
   var libres = surf.TrouverSi(exclure,c => c == default || c == ' ');
   for (int r = 0; r < robotsInfo.Length; r++)
   {
      RobotInfo info = robotsInfo[r];
      int n = dé.Next(libres.Count);
      Robot robot =
         CréerRobot(info.nom, libres[n], info.couleur, info.catégorie);
      info.robot = robot;
      libres.RemoveAt(n);
   }
   return robots;
}
static Robot
   CréerRobot(string nom, Point2D pos, ConsoleColor couleur, Catégorie cat)
{
   Robot robot = new(nom, pos, couleur);
   robot.Équiper(new Détecteur(robot, 1, cat));
   return robot;
}

static List<Déchet>
   CréerPollution((char sym, Catégorie cat)[] symCatégorie, Random dé,
                  int nbDéchets,
                  Surface surf, Func<Point2D, bool> exclure)
{
   List<Déchet> déchets = new();
   var libres = surf.TrouverSi(exclure, c => c == default || c == ' ');
   if (libres.Count < nbDéchets * symCatégorie.Length)
      throw new SurfacePleineException();
   foreach (var symCat in symCatégorie)
   {
      for (int d = 0; d != nbDéchets; ++d)
      {
         int n = dé.Next(libres.Count);
         déchets.Add(new(symCat.sym, symCat.cat, libres[n]));
         libres.RemoveAt(n);
      }
   }
   return déchets;
}

static Thread[] CréerCueilleurs
   (RobotInfo[] robotsInfo, Surface surf, CanalComm canal,
    Messagerie messagerie, Messagerie information, CancellationToken jeton)
{
   Dictionary<char, int> collectés = new();
   var cueilleurs = new Thread[robotsInfo.Length];
   for (int r = 0; r < robotsInfo.Length; r++)
   {
      var (robot, noPort, _, _, cat) = robotsInfo[r];
      var catalogue = Catalogue.Get;
      cueilleurs[r] = new (() =>
      {
         // tant qu'il reste des déchets à ramasser
         while (!jeton.IsCancellationRequested &&
                surf.TrouverSi(c => catalogue.Est(c, cat)).Count > 0)
         {
            bool trouvé = false;
            // Trouver et ramasser le déchet
            do
            {
               canal.PublierSur(noPort, new(robot.Zone, robot.Couleur, robot.Symbole));
               var pts = robot.Détecter(surf, cat);
               if (pts.Count > 0)
               {
                  trouvé = true;
                  if (pts[0] == robot.Position)
                  {
                     char c = surf[pts[0]].Symbole;
                     lock (collectés)
                        if (collectés.ContainsKey(c))
                           collectés[c]++;
                        else
                           collectés.Add(c, 1);
                     var pos = robot.Position;
                     messagerie.Ajouter
                     (
                        robot.Couleur,
                        $"Déchet collecté à la position {pos}"
                     );
                     surf.Retirer(pos);
                  }
                  else
                  {
                     messagerie.Ajouter
                     (
                        robot.Couleur,
                        $"Trouvé {pts.Count} déchet(s)",
                        $"Déplacement vers {pts[0]}"
                     );
                     robot.DéplacerVers(pts[0]);
                     robot.RéinitialiserPuissance();
                  }
                  string s = "Collectés : ";
                  lock (collectés)
                  {
                     foreach (var (sym, n) in collectés)
                        s += $"{sym} x {n}; ";
                     information.Ajouter(ConsoleColor.White, s, "Pressez une touche pour terminer");
                  }
               }
               else
               {
                  robot.AugmenterPuissance();
               }
               Thread.Sleep(500);
            }
            while (!trouvé);
            robot.RéinitialiserPuissance();
         }
      });
   }
   return cueilleurs;
}

class RobotInfo
{
   public Robot? robot;
   public int noPort;
   public string nom;
   public ConsoleColor couleur;
   public Catégorie catégorie;
   public RobotInfo(Robot? robot, int noPort, string nom,
                    ConsoleColor couleur, Catégorie catégorie)
   {
      this.robot = robot;
      this.noPort = noPort;
      this.nom = nom;
      this.couleur = couleur;
      this.catégorie = catégorie;
   }
   public void Deconstruct(out Robot? robot, out int noPort,
                           out string nom, out ConsoleColor couleur,
                           out Catégorie catégorie)
   {
      robot = this.robot;
      noPort = this.noPort;
      nom = this.nom;
      couleur = this.couleur;
      catégorie = this.catégorie;
   }
}

class SurfacePleineException : Exception;

À noter :

  • Le type RobotInfo qui offre les informations descriptives pour un Robot, de même que la variable robotsInfo utilisée pour décrire les instances de Robot pour cette simulation
  • La variable symCatégorie qui explique à quelle catégorie chaque symbole de déchet devra être associé
  • La présence de deux instances de Messagerie, soit messagerie (qui sera à la droite de surf, qui sera la Surface de simulation) et information (qui se trouvera juste en dessous de surf)
  • La fabrique CréerRobots, qui crée des robots sur la base de plusieurs RobotInfo
  • La fabrique CréerRobot qui crée un Robot sur la base de quelques informations clés
  • La fabrique CréerPollution, qui... crée de la pollution
  • La fabrique CréerCueilleurs, qui crée les fils d'exécution qui contrôlent les robots et les démarre
  • Le fil d'exécution lireTouche, qui lit une touche et arrête l'exécution du programme

Si vous avez eu de la difficulté avec le TP01, vous trouverez une implémentation fonctionnelle sur TP01-Wallyd.zip mais notez que vous n'avez absolument aucune obligation de l'utiliser : vous pouvez utiliser, comme base de travail, votre propre TP01, ou la version proposée par vos chics profs, ou encore un savant hybride des deux... L'important est que vous compreniez ce que vous faites!

Important : je sais que plusieurs d'entre vous utilisez les tabulations par défaut de Visual Studio, qui correspondent à quatre espaces. Ça ne me dérange pas, mais personnellement ça m'agace alors j'utilise une indentation à trois espaces. Quand les cours se donnaient sur les postes du Collège, nous les faisions préparer en conséquence!

Si vous utilisez mes sources à titre de point de départ, assurez-vous que les fichiers seront correctement indentés (Édition → Avancé → Mettre le document en forme) avant de commencer à programmer, sinon le nouveau code sera indenté de quatre espaces, le code existant sera indenté de trois espaces, et le tout sera difficile à comprendre. Vous ne voudriez pas perdre des points dû à une indentation incorrecte, n'est-ce pas?

TP02b

Vendredi le 6 décembre à 23 h 59

Le code du programme principal imposé mais incomplet est le suivant :

// ...
RobotInfo[] robotsInfo =
{
   new(null, 0, "Wallyd", ConsoleColor.Magenta, Catégorie.Métal),
   new(null, 1, "Bart", ConsoleColor.Blue, Catégorie.Plastique),
   new(null, 2, "Charles", ConsoleColor.Yellow, Catégorie.Organique),
   new(null, 3, "Lisa", ConsoleColor.Green, Catégorie.Nucléaire)
};

(char sym, Catégorie cat)[] symCatégorie =
{
    ('%', Catégorie.Métal),
    ('*', Catégorie.Plastique),
    ('&', Catégorie.Plastique),
    ('/', Catégorie.Organique),
    ('?', Catégorie.Nucléaire),
    ('*', Catégorie.Recyclable),
};
foreach (var symCat in symCatégorie)
   Catalogue.Get.Associer(symCat.sym, symCat.cat);

// préparer la surface d'affichage
const int HAUTEUR = 20,
          LARGEUR = 40;
Surface surf = new(HAUTEUR, LARGEUR);
// préparer le cadre de la surface
Cadre cadre = new(HAUTEUR, LARGEUR, ConsoleColor.Cyan);
// préparer la zone de messagerie
Messagerie messagerie = new(new(LARGEUR, 0), HAUTEUR);
// préparer la zone d'informations
Messagerie information = new(new(0, HAUTEUR), 2);

Random dé = new();
List<(Robot r, Catégorie c)> robots =
   CréerRobots(robotsInfo, dé, surf, cadre.Exclure);

// placer les déchets sur la surface
const int NB_DÉCHETS = 10;
List<Déchet> déchets =
   CréerPollution(symCatégorie, dé, NB_DÉCHETS, surf, cadre.Exclure);
surf.Ajouter(déchets.ToArray());

CanalComm<InfoCanalComm> canal = new(robotsInfo.Length);
PipelineAffichage pipe = new(surf, cadre, canal);

CancellationTokenSource cts0 = new();
CancellationTokenSource cts1 = new();

Task collectes =  Task.WhenAny
(
   ExécuterCollecteAsync
   (
      robotsInfo, surf, canal, messagerie, information, cts0.Token, cts1
   ), ArrêterAsync(cts0)
);

while (!cts1.Token.IsCancellationRequested)
{
   pipe.Rafraichir();
   messagerie.Afficher();
   information.Afficher();
}
pipe.Rafraichir();
messagerie.Afficher();
information.Afficher();

// Ici la méthode ExécuterCollecteAsync

// Ici la méthode ArrêterAsync


static List<(Robot robot, Catégorie cat)>
   CréerRobots(RobotInfo[] robotsInfo, Random dé, Surface surf, Func<Point2D, bool> exclure)   
{
   List<(Robot r, Catégorie c)> robots = new();
   // Trouver les cases libres
   var libres = surf.TrouverSi(exclure, c => c == default || c == ' ');
   for (int r = 0; r < robotsInfo.Length; r++)
   {
      RobotInfo info = robotsInfo[r];
      int n = dé.Next(libres.Count);
      Robot robot =
         CréerRobot(info.nom, libres[n], info.couleur, info.catégorie); 
      info.robot = robot;
      libres.RemoveAt(n);
   }
   return robots;
}
static Robot
   CréerRobot(string nom, Point2D pos, ConsoleColor couleur, Catégorie cat)
{
   Robot robot = new(nom, pos, couleur);
   robot.Équiper(new Détecteur(robot, 1, cat));
   return robot;
}
static List<Déchet>
   CréerPollution((char sym, Catégorie cat)[] symCatégorie, Random dé,
                  int nbDéchets,
                  Surface surf, Func<Point2D, bool> exclure)
{
   List<Déchet> déchets = new();
   var libres = surf.TrouverSi(exclure, c => c == default || c == ' ');
   if (libres.Count < nbDéchets * symCatégorie.Length)
      throw new SurfacePleineException();
   foreach (var symCat in symCatégorie)
   {
      for (int d = 0; d != nbDéchets; ++d)
      {
         int n = dé.Next(libres.Count);
         déchets.Add(new(symCat.sym, symCat.cat, libres[n]));
         libres.RemoveAt(n);
      }
   }
   return déchets;
}
class RobotInfo
{
   public Robot? robot;
   public int noPort;
   public string nom;
   public ConsoleColor couleur;
   public Catégorie catégorie;
   public RobotInfo(Robot? robot, int noPort, string nom,
                    ConsoleColor couleur, Catégorie catégorie)
   {
      this.robot = robot;
      this.noPort = noPort;
      this.nom = nom;
      this.couleur = couleur;
      this.catégorie = catégorie;
   }
   public void Deconstruct(out Robot? robot, out int noPort,
                           out string nom, out ConsoleColor couleur,
                           out Catégorie catégorie)
   {
      robot = this.robot;
      noPort = this.noPort;
      nom = this.nom;
      couleur = this.couleur;
      catégorie = this.catégorie;
   }
}
class SurfacePleineException : Exception { }
// ...

PFI

   

Solutionnaires

Solutionnaire de la classe Messagerie proposée à la séance S17 :

using System;
using System.Collections.Generic;

namespace ActivitéArdoise
{
   class HauteurInvalideException : Exception { }
   class Messagerie : IObservateurArdoise
   {
      Dictionary<string, List<string>> Messages { get; } = new();
      Dictionary<string, ConsoleColor> Couleurs { get; } = new();
      static readonly ConsoleColor[] couleurs =
      {
         ConsoleColor.Red, ConsoleColor.Green, ConsoleColor.Blue,
         ConsoleColor.Yellow, ConsoleColor.Cyan, ConsoleColor.Magenta
      };
      int Hauteur { get; init; }
      public Messagerie(int hauteur)
      {
         if (hauteur <= 1) throw new HauteurInvalideException();
         Hauteur = hauteur;
      }
      public void Nouveauté(string qui, string quoi)
      {
         lock (this)
         {
            if (Messages.ContainsKey(qui))
            {
               Messages[qui].Add(quoi);
               if (Messages[qui].Count > Hauteur)
                  Messages[qui].RemoveAt(0);
            }
            else
            {
               Messages.Add(qui, new());
               Couleurs.Add(qui, couleurs[Messages.Count % couleurs.Length]);
            }
         }
      }
      public void Afficher()
      {
         int[] largeurs;
         string[] noms;
         List<string> [] messages;
         lock (this)
         {
            largeurs = new int[Messages.Count];
            noms = new string[Messages.Count];
            messages = new List<string>[Messages.Count];
            int i = 0;
            foreach (var (qui, msgs) in Messages)
            {
               noms[i] = qui;
               largeurs[i] = Algos.Cumuler(
                  msgs, (cur, s) => Math.Max(cur, s.Length), noms[i].Length
               );
               messages[i] = new(msgs);
               ++i;
            }
         }
         // titres
         Console.Clear();
         for (int i = 0; i != largeurs.Length; ++i)
            Console.Write(noms[i].PadRight(largeurs[i]));
         Console.WriteLine();
         // messages
         for (int j = 0; j != Hauteur; ++j)
         {
            for (int i = 0; i != largeurs.Length; ++i)
            {
               string nom = noms[i];
               if (j < messages[i].Count)
               {
                  ConsoleColor pré = Console.ForegroundColor;
                  Console.ForegroundColor = Couleurs[nom];
                  Console.Write(messages[i][j].PadRight(largeurs[i]));
                  Console.ForegroundColor = pré;
               }
               else
                  Console.Write(new string(' ', largeurs[i]));
            }
            Console.WriteLine();
         }
      }
   }
}

En espérant que ça vous aide à organiser vos idées!


Valid XHTML 1.0 Transitional

CSS Valide !