420-SF2-RE – Structures de données et programmation orientée objet

Quelques raccourcis :

Ceci est un petit site de support pour le cours 420-SF2-RE – Structures de données et programmation orientée objet.

Vous trouverez aussi des liens sur divers langages (dont C#, notre outil de prédilection dans ce cours) un peu partout dans https://h-deb.ca/

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 un résumé des principales règles de programmatique en vigueur dans ce cours.

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

Détail des séances en classe

Date Séance Détails

22 janvier

S00

Au menu :

  • Présentation du cours et du plan de cours
  • Petit exercice de remise en forme

Notre exercice fut :

Écrivez un programme console C# qui :

  • Lit une hauteur de rectangle, qui doit être entre 1 et 20 inclusivement
  • Lit une largeur de rectangle, qui doit être entre 1 et 70 inclusivement
  • Lit un symbole qui servira à dessiner le rectangle. Ce symbole sera un caractère non-blanc (voir char.IsWhiteSpace pour valider ceci)

Ensuite, dessinez à l'écran le rectangle correspondant

Nous en avons profité pour faire une brève révision de 420SF1, puis pour apporter des ajustements menant à un glissement vers la matière au menu de notre cours.

Notre version initiale fut :

int hauteur = LireHauteur();
int largeur = LireLargeur();
char symbole = LireSymbole();
DessinerRectangle(hauteur, largeur, symbole);

static void DessinerRectangle(int hau, int lar, char sym)
{
   for(int ligne = 0; ligne != hau; ++ligne)
   {
      for(int col= 0; col != lar; ++col)
      {
         Console.Write(sym);
      }
      Console.WriteLine();
   }
}

static char LireSymbole()
{
   Console.Write("Symbole? ");
   char symbole = char.Parse(Console.ReadLine());
   while(char.IsWhiteSpace(symbole))
   {
      Console.WriteLine($"Entrée erronnée. Symbole? ");
      symbole = char.Parse(Console.ReadLine());
   }
   return symbole;
}


static int LireEntierBorné(string msg, int min, int max)
{
   int valeur;
   Console.Write($"{msg}? ");
   valeur = int.Parse(Console.ReadLine());
   while (!EstEntreInclusif(valeur, min, max))
   {
      Console.WriteLine($"Entrée erronnée. {msg}? ");
      valeur = int.Parse(Console.ReadLine());
   }
   return valeur;
}
static int LireHauteur()
{
   const int HAUTEUR_MIN = 1,
             HAUTEUR_MAX = 20;
   return LireEntierBorné("Hauteur", HAUTEUR_MIN, HAUTEUR_MAX);
}
static int LireLargeur()
{
   const int LARGEUR_MIN = 1,
             LARGEUR_MAX = 70;
   return LireEntierBorné("Largeur", LARGEUR_MIN, LARGEUR_MAX);
}
static bool EstEntreInclusif(int val, int min, int max)
{
   return min <= val && val <= max;
}

Nous avons ensuite introduit l'idée d'une classe Rectangle, pour que le code qui dessine des rectangles ait bel et bien... des rectangles! Le code du programme principal résultant fut :

using z;
Rectangle[] rects = new Rectangle[]
{
   new (5, 12, 'L'), new(7, 20, '0'), new(17, 43, '*')
};
for (int i = 0; i != rects.Length; ++i)
   rects[i].Dessiner();

... et celui de la classe Rectangle en tant que telle fut :

namespace z
{
   internal class Rectangle
   {
      // variables membres : attributs
      int hauteur;
      int largeur;
      char symbole;
      // propriété : mécanisme de contrôle d'accès
      // de fine granularité aux états d'un objet
      public int Hauteur
      {
         // permet de consulter la variable en lecture
         get { return hauteur; }
         private init /*set*/ // permet de modifier la variable
         {
            hauteur = value;
         }
      }
      public int Largeur
      {
         get { return largeur; }
         private init /*set*/ { largeur = value; }
      }
      public char Symbole
      {
         get { return symbole; }
         private set { symbole = value; }
      }

      // constructeur : méthode qui sert à placer
      // l'objet nouvellement construit dans un état
      // initial correct
      public Rectangle(int hau, int lar, char sym)
      {
         Hauteur = hau;
         Largeur = lar;
         Symbole = sym;
      }
      // méthode d'instance Dessiner, qui a pour rôle
      // d'afficher _ce_ Rectangle à la console
      public void Dessiner()
      {
         for (int ligne = 0; ligne != hauteur; ++ligne)
         {
            for (int col = 0; col != largeur; ++col)
            {
               Console.Write(symbole);
            }
            Console.WriteLine();
         }
      }
   }
}

Notez qu'en ce premier cours, nous investissons nos efforts sur deux aspects, soit :

  • Reprendre la forme intellectuelle après le congé des Fêtes, et
  • Mettre en place le vocabulaire et les idées qui guideront nos réflexions en ce début de session

Ça fait un gros cours pour démarrer la session, mais nous réinvestirons le tout cours après cours, alors ça va entrer doucement dans notre tête, et s'intégrer à notre praxis émergente 🙂.

  • Mise en place du vocabulaire de base de la POO, mais sous forme d'un survol seulement, et avec accent sur l'encapsulation (sans la nommer, mais vous ne perdez rien pour attendre!). Ainsi, nous avons identifié et situé les termes et idées suivant(e)s :
    • une maxime importante : « un objet est responsable de son intégrité, du début à la fin de sa vie »
    • quelques mots clés qui aident à encadrer la capacité d'un objet à assurer son encapsulation, soit les qualifications d'accès private et public
      • nous n'avons pas vraiment parlé de protected, qui a une utilité réelle mais plus limitée que des deux autres, et dont nous ne pouvons pas traiter pour le moment
      • nous avons toutefois insisté sur le fait que private, la qualification par défaut des membres dans une classe, est ce que nous souhaitons utiliser le plus possible, et nous avons donné quelques raisons pour soutenir cette prise de position
      • nous avons démontré que public, du moins pour les états d'un objet, devrait être évité dans la plupart des cas, cette qualification empêchant de facto l'objet d'assurer son encapsulation
  • Nous avons mis de l'avant quelques mots de vocabulaire plus techniques, soit :
    • les méthodes, qui sont les services (les fonctions, les comportements) d'un objet
    • les attributs, qui sont les états (variables, constantes) d'un objet
    • les constructeurs, qui sont les méthodes un peu spéciales mais Ô combien importantes qui permettent de déterminer l'état initial d'un objet – sans eux, pas d'état initial connu pour un objet, donc pas d'encapsulation pour lui
  • Nous avons discuté du glissement sémantique entre F(obj), où on applique une opération sur une entité , ce qui rejoint l'approche procédurale mise en application dans le cours 420-SF1-RE, et obj.F(), qui sollicite la méthode de l'objet et rend ce dernier actif
  • Nous avons discuté brièvement des accesseurs (méthodes de consultation) et des mutateurs (méthodes de modification), qui sont des familles de services typiques pour les objets
    • La partie get d'une propriété est un exemple d'accesseur
    • La partie set d'une propriété est un exemple de mutateur
    • La partie init d'une propriété est aussi un exemple de mutateur, mais qui ne fonctionne que durant la construction d'un objet. Évidemment, une propriété ne peut pas avoir à la fois un set et un init (c'est l'un ou l'autre)
    • Il est évidemment possible d'écrire des méthodes qui ont on comportement d'accesseur ou un comportement de mutateur
  • Nous avons montré que C# permet une écriture concise pour ces deux familles importantes de services, soit les propriétés, et nous avons indiqué qu'il s'agit d'une pratique idiomatique dans ce langage, donc d'une pratique que nous allons mettre en application même si elle est plus « décorative » que nécessaire
  • Nous avons abordé très sommairement le mot static, qui en C# a le sens de « membre de classe » alors qu'un membre qui n'est pas qualifié static est un « membre d'instance »
// ...
Triangle t = new(5, 'A');
t.Dessiner();
// ...
    ... affichera à la console la chose suivante :
    A
   AAA
  AAAAA
 AAAAAAA
AAAAAAAAA

En fait, notre Triangle n'était pas centré lors de l'affichage, mais considérez cet ajout comme un bonbon!

À titre de « petit bonbon », nous avons vu en début de séance que les fonctions qui se limitent à un seul return peuvent être écrites de manière simplifiée, et nous avons appris que votre chic prof, souhaitant vous encourager à écrire de courtes fonctions qui font une et une seule chose (et le font bien!), acceptera cette syntaxe si elle est bien utilisée.

Ainsi, ceci :

static int Carré(int n)
{
   return n * n;
}

... peut s'écrire de manière équivalente sous la forme suivante :

static int Carré(int n) => n * n;

... alors que ceci :

class Nom
{
   const string NOM_DÉFAUT = "Inconnu(e)";
   string valeur;
   public string Valeur
   {
      get
      {
         return valeur;
      }
      private set
      {
         if (value != null && value.Length > 0)
         {
            valeur = value;
         }
         else
         {
            valeur = NOM_DÉFAUT;
         }
      }
   }
   public string Crié
   {
      get
      {
         return Valeur.ToUpper();
      }
   }
   public Nom(string valeur)
   {
      Valeur = valeur;
   }
}

... pourra s'écrire comme suit 

class Nom
{
   const string NOM_DÉFAUT = "Inconnu(e)";
   string valeur;
   public string Valeur
   {
      get => valeur;
      private set
      {
         if (value != null && value.Length > 0)
         {
            valeur = value;
         }
         else
         {
            valeur = NOM_DÉFAUT;
         }
      }
   }
   public string Crié => Valeur.ToUpper(); // note : un get seulement, pas de set
   public Nom(string valeur)
   {
      Valeur = valeur;
   }
}

D'autres simplifications d'écriture viendront plus tard dans la session.

Quelques nouveaux termes de vocabulaire utilisés aujourd'hui : classe, instance, attribut d'instance, propriété d'instance (avec volets get, set et init), constructeur par défaut, constructeur paramétrique, qualifications d'accès private et public (il y en a d'autres), encapsulation (à peine), membre d'instance (non-static), membre de classe : static, invariant, précondition et postcondition... Ouf!

26 janvier

S01

Au menu :

  • Retour sur l'idée de propriété
    • Accesseur (volet get)
    • Mutateur à la construction seulement (volet init)
    • Mutateur en tout temps (volet set)
    • Pourquoi init est souvent préférable à set (et comment choisir!)
  • Membre d'instance (non-static) ou membre de classe (static)?
  • Poursuite de la mise en place du vocabulaire de base de la POO, mais sous forme d'un survol seulement, et avec accent sur l'encapsulation. Ainsi, nous avons identifié et situé les termes et idées suivant(e)s :
    • Respect des invariants, entre chaque appel d'un service d'un objet
    • Identification et garantie du respect des préconditions pour les services d'un objet
    • Identification et garantie du respect des postconditions pour les services d'un objet
    • Rappel d'une maxime importante : « un objet est responsable de son intégrité, du début à la fin de sa vie »
  • Petite séance de programmation collective. Écrivons ensemble un programme qui :
    • Lit un nombre de personnes. Ce nombre doit être un entier strictement positif
    • Pour chaque personne :
      • lit son nom, qui devra être une chaîne de caractères non-vide
      • lit son âge (un entier positif, donc supérieur ou égal à zéro)
      • crée une Personne ayant ce nom et cet âge
      • l'ajoute dans un tableau
  • Une fois toutes les personnes lues, ce programme :
    • Trouvera et affichera le nom de la personne dont le nom est le plus long
    • Affichera l'âge moyen des personnes
  • Pour y arriver, nous définirons une classe Personne telle que :
    • Une Personne aura un nom et un âge
    • Le nom d'une Personne ne pourra être vide. Par défaut, nous utiliserons le nom "INCONNU(E)"
    • L'âge d'une Personne devra se situer entre 0 et 140 inclusivement. Par défaut, nous considérerons qu'une Personne est d'âge zéro

Quelques nouveaux termes de vocabulaire utilisés aujourd'hui : invariant, précondition et postcondition

Une solution possible est disponible ici. Vous remarquerez que les mutateurs (les volets set et init des propriétés) sont exprimés un peu différemment sur cet exemple que ce que nous avons fait en classe, mais je vous explique ce que ça signifie dès la séance S02.

À titre de bonbons aujourd'hui, j'ai aussi montré :

  • Le côté optionnel des accolades dans certaines structures de contrôle (if, for, while) quand le corps se limite à une seule instruction
  • Le droit à placer plus d'un return par fonction, mais seulement si c'est justifié

29 janvier

S02

Au menu :

  • Que faire pour signaler qu'une fonction ne pourra pas rencontrer ses postconditions : introduction (très brève) aux exceptions, un sujet important que nous abordons aujourd'hui mais aussi sur lequel nous reviendrons bientôt, et au mot clé throw

À titre de bonbon aujourd'hui, j'ai aussi montré :

  • Comment utiliser foreach dans le cas où une répétitive doit itérer sur tous les éléments d'une séquence et n'a pas besoin de la valeur des indices
  • Le côté optionnel des accolades dans certaines structures de contrôle (foreach, mais pas try ni catch) quand le corps se limite à une seule instruction
  • Comment utiliser l'opérateur ternaire dans les cas où nous souhaitons choisir l'une de deux expressions de même type sur la base d'une condition
  • Présentation du labo 00 – Le cryptographe
  • Travail sur le labo 00 – Le cryptographe

Quelques trucs pour vous aider...

Si vous cherchez un exemple simpliste de programme de test interactif, vous pouvez utiliser celui-ci (vous pouvez aussi vous en écrire un plus à votre goût; je ne ramasserai que la classe Crypteur après tout) :

// ...
do
{
   Console.Write("Message à chiffrer? ");
   string texte = Console.ReadLine();
   Console.Write("Clé de chiffrement? ");
   int cléChiffrement = int.Parse(Console.ReadLine());
   Crypteur crypteur = new (texte, cléChiffrement);
   Console.Write("Clé à utiliser pour déchiffrer? ");
   int cléDéchiffrement = int.Parse(Console.ReadLine());
   Console.WriteLine($"Message \"déchiffré\" : {crypteur.Déchiffrer(cléDéchiffrement)}");
}
while (Poursuivre());
// ...

... notez que je suppose que vous pouvez écrire la fonction Poursuivre puisque nous l'avons fait à quelques reprises cet automne.

Si vous souhaitez transformer une string en char[], examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer une string en majuscules, examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer un char[] en string, sachez que string expose un constructeur paramétrique acceptant un char[] en paramètre. Par exemple, comparez les deux exemples ci-dessous (l'un des deux est plus pertinent que l'autre pour vos fins) :

// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = cs.ToString();
Console.Write(s);
// ...
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = new string(cs);
Console.Write(s);
// ...

Si vous souhaitez modifier un char, alors... que diriez-vous de la simple arithmétique? Par exemple :

// ...
char c = 'A';
Console.WriteLine(c); // A
c = (char)(c + 3);
Console.WriteLine(c); // D
// ...

Quelques trucs pour vous aider...

Si vous cherchez un exemple simpliste de programme de test interactif, vous pouvez utiliser celui-ci (vous pouvez aussi vous en écrire un plus à votre goût; je ne ramasserai que la classe Crypteur après tout) :

// ...
do
{
   Console.Write("Message à chiffrer? ");
   string texte = Console.ReadLine();
   Console.Write("Clé de chiffrement? ");
   int cléChiffrement = int.Parse(Console.ReadLine());
   Crypteur crypteur = new (texte, cléChiffrement);
   Console.Write("Clé à utiliser pour déchiffrer? ");
   int cléDéchiffrement = int.Parse(Console.ReadLine());
   Console.WriteLine($"Message \"déchiffré\" : {crypteur.Déchiffrer(cléDéchiffrement)}");
}
while (Poursuivre());
// ...

... notez que je suppose que vous pouvez écrire la fonction Poursuivre puisque nous l'avons fait à quelques reprises cet automne.

Si vous souhaitez transformer une string en char[], examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer une string en majuscules, examinez les méthodes d'instance de cette string (portez attention en particulier à celles dont le nom commence par To...).

Si vous souhaitez transformer un char[] en string, sachez que string expose un constructeur paramétrique acceptant un char[] en paramètre. Par exemple, comparez les deux exemples ci-dessous (l'un des deux est plus pertinent que l'autre pour vos fins) :

// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = cs.ToString();
Console.Write(s);
// ...
// ...
char [] cs = { 'a', 'l', 'l', 'o' };
string s = new string(cs);
Console.Write(s);
// ...

Si vous souhaitez modifier un char, alors... que diriez-vous de la simple arithmétique? Par exemple :

// ...
char c = 'A';
Console.WriteLine(c); // A
c = (char)(c + 3);
Console.WriteLine(c); // D
// ...

2 février

S03

Au menu :

  • Comment demander de l'aide
  • Comment convertir un objet en string, et pourquoi, si tabChar est un char[], l'expression tabChar.ToString() ne donne pas le même résultat que l'expression new string(tabChar) (note : nous y reviendrons)
  • Bref rappel sur les exceptions, qui permettent entre autres de découpler la détection d'une situation atypique (une erreur, dans la grande majorité des cas) de son traitement. Plus en détail :
    • Pourquoi distinguer le moment / le lieu où un problème est détecté du moment / du lieu où il est traité (le cas échéant)
    • Trois mots clés : throw (signaler une situation exceptionelle), try (exécuter du code à risque, avec pour objectif de réagir si un problème survient), catch (gérer la situation signalée)
      • Un quatrième mot, finally, est très important (plus que catch!), mais nous y reviendrons
    • Exceptions et préconditions
    • Exceptions et postconditions
    • Exceptions et respect des invariants
    • Exceptions et constructeurs
  • Définir un type d'exception « maison »
    • Introduction à la syntaxe (qui, en C#, implique l'héritage d'implémentation, sujet que nous ne ferons qu'effleurer aujourd'hui)
  • Revisiter des exemples vus précédemment à la lueur de la matière d'aujourd'hui
  • De plus :
    • jasette informelle sur le choix d'une université (c'est le temps des portes ouvertes!)
    • mesurer l'impact en temps de levées d'exceptions
    • mesurer l'impact en temps d'ajouter un caractère à une string
    • pourquoi le type string est immuable en C#
    • pour celles et ceux qui ne l'ont pas vu la session précédente : la méthode TryParse
  • Quelques exemples concrets :
    • division entière avec levée d'exception
    • coût des exceptions
    • coût de l'ajout de caractères à une string (version naïve)
    • classe Cercle avec invariants (voir la consigne ci-dessous)
    • manipulation de string et passage de string à char[] (et inversement)

Rédigez la classe Cercle telle que :

  • Un Cercle est représenté par un Centre et un Rayon
  • Le Centre est un Point
  • Le Rayon est un nombre à virgule flottante supérieur à zéro
  • Un Cercle est immuable
  • Un Cercle offre une méthode Contient acceptant en paramètre un Point et retournant true seulement si ce Point est dans le Cercle (bordure incluse)

À titre de bonbon aujourd'hui, j'ai aussi montré :

  • Comment faire des propriétés automatiques, dans les cas où une classe n'a pas d'invariant à garantir
  • Comment un constructeur peut déléguer une partie (ou la totalité) de son travail à un autre constructeur
class Point
{
   public float X { get; private init; }
   public float Y { get; private init; }
   public Point() : this(0,0)
   {
   }
   public Point(float x, float y)
   {
      X = x;
      Y = y;
   }
   public float DistanceDe(Point autre) =>
      (float) Math.Sqrt(Math.Pow(X - autre.X, 2) + Math.Pow(Y - autre.Y, 2));
}
class RayonInvalideException : Exception;
class Cercle
{
   public Point Centre { get; init; }
   private float rayon;
   private static bool EstRayonValide(float candidat) => candidat > 0;
   public float Rayon
   {
      get => rayon;
      set
      {
         rayon = EstRayonValide(value)? value : throw new RayonInvalideException();
      }
   }
   public Cercle() : this(1)
   {
   }
   public Cercle(float rayon) : this(new(), rayon)
   {
   }
   public Cercle(Point centre, float rayon)
   {
      Centre = centre;
      Rayon = rayon;
   }
   public bool Contient(Point pt) => Centre.DistanceDe(pt) <= Rayon;
}
  • Exemple (simpliste) de manipulation de chaînes de caractères :
string s = "J'aime mon prof";
Console.WriteLine(s);
s = s.ToUpper();
Console.WriteLine(s);
//for(int i = 0; i != s.Length; ++i)
//   Console.Write($"{s[i]} ");
foreach (char c in s)
   Console.Write($"{c} ");
Console.WriteLine();
char[] tab = s.ToArray();
for (int i = 0; i != tab.Length; ++i)
   if (char.IsWhiteSpace(tab[i]))
      tab[i] = '#';
// notez la différence entre les deux conversions ci-dessous...
s = new string(tab);
Console.WriteLine(s);
s = tab.ToString();
Console.WriteLine(s);

5 février

S04

Au menu :

  • Minitest Q00
  • Dernière séance pour fignoler le labo 00. C'est le moment idéal pour :
    • Vous assurer de respecter les consignes (relisez-les avec attention!)
    • Tester les cas limites (p. ex. : notre Crypteur rejette les chaînes vides, mais il reste que chiffrer une chaîne vide n'est pas une erreur... Ça donne simplement une autre chaîne vide alors faudrait que votre algorithme tienne la route!)
    • Si vous ne l'avez pas encore fait, vous pouvez essayer les tests proposés dans l'énoncé... Si vous ne passez pas tous les tests, c'est qu'il vous reste encore au moins un bogue (et si vous passez tous les tests, ça ne veut pas dire que le code est propre! 🙂)

Vous pouvez aussi essayer le programme de test suivant (ajoutez le using requis pour que votre Crypteur soit accessible) :

/// Programme client de tests pour le laboratoire 00
/// classe Crypteur - Hiver 2024
/// 
/// par Vincent Echelard, 2013
/// révisé et amélioré par Pierre Prud'homme, février 2022
/// Ajusté et mis à jour par Patrice Roy, février 2023
/// --------------------------------------------------------------------
using System;

Console.SetWindowSize(Console.LargestWindowWidth, Console.LargestWindowHeight);
Console.SetWindowPosition(0,0);
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.Clear();

const int NB_TESTS = 10;
Console.Write("Nom de l'étudiant(e) : ");
string s = Console.ReadLine();
Console.WriteLine(new string('-', 72));

int résultatTests = 0;
résultatTests += Tester("ALLO", 2, "CNNQ", "Test simple (majuscules)");
résultatTests += Tester("Allo", 2, "CNNQ", "Test simple (majuscule/minuscules)");
résultatTests += Tester("allo", 2, "CNNQ", "Test simple (minuscules)");
résultatTests += Tester("allo", -2, "CNNQ", "Test clef négative (chiffrement)");
résultatTests += Tester("ALLO", 1328, "CNNQ", "Test clef très grande (chiffrement)");
résultatTests += Tester("Veux-tu 1 café?", 2, "XGWZ-VW 1 ECHÉ?", "Test caractères non-alphabétique (chiffrement)");
// oui, il y a une faute dans la phrase suivante, je sais
résultatTests += Tester("Zorglub veux 1 café!", -1332, "FUXMRAH BKAD 1 IGLÉ!", "Test complet (chiffrement)");
résultatTests += Tester("Élu par cette crapule", 27, "ÉMV QBS DFUUF DSBQVMF", "Test décalage +1");
résultatTests += Tester("Élu par cette crapule", 26, "ÉLU PAR CETTE CRAPULE", "Test de chiffrement sans effet");
résultatTests += Tester("élu par cette crapule", 25, "ÉKT OZQ BDSSD BQZOTKD", "Test décalage -1");
Console.Write($"A eu : {résultatDesTests} / {NB_TESTS}");
Console.ReadKey();

static int Tester(string messageÀCrypter, int clefDeChiffrement,
                  string messageCrypté, string nomDuTest)
{
   string espacement = new string(' ', 13);
   int résultat = 0;
   Crypteur objTest = null;

   try
   {
      objTest = new Crypteur(messageÀCrypter, clefDeChiffrement);
      if (objTest.MessageSecret == messageCrypté)
      {
         résultat = 1;
         Console.WriteLine($"Réussite --> {nomDuTest} " + 
                           $"Msg à crypter: {messageÀCrypter} - " +
                           $"Clef: {clefDeChiffrement}  " + Environment.NewLine +
                           $"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
      }
      else
         Console.WriteLine($"ÉCHEC    --> {nomDuTest} " +
                           $"Msg à crypter: {messageÀCrypter} - " +
                           $"Clef: {clefDeChiffrement}  " + Environment.NewLine +
                           $"{espacement}Prévu:{messageCrypté} - Obtenu:{objTest.MessageSecret} " + Environment.NewLine);
   }
   catch (Exception e)
   {
      Console.WriteLine($"Erreur lors du test " + nomDuTest + " : " + e.Message);
      if (objTest != null)
         Console.WriteLine($"Msg à crypter : {messageÀCrypter}  - Clef : {clefDeChiffrement}   -- Msg obtenu : {objTest.MessageSecret}");
   }
   return résultat;
}

Pour le reste : des exercices, des exercices, et des exercices!

Exercice 1a

Soit une classe représentant un point de l'espace 2D. Chaque instance de cette classe :

  • Comporte deux attributs « réels » : un x et un y
  • Donne accès en lecture aux valeurs de x et y par le moyen d'une propriété X et Y respectivement
  • Donne accès en écriture aux valeurs de x et de y par le moyen des propriétés X et Y sans les valider puisque toute valeur est acceptable
  • Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide
  • Comporte un constructeur par défaut
  • Offre une fonction pour calculer la distance entre deux points

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la à l'aide du code client disponible sur le site Web du cours.

Exercice 1b

Soit une classe représentant un point du quadrant 1 de l'espace 2D. Chaque instance de cette classe :

  • Comporte deux attributs « réels » : un x et un y
  • Donne accès en lecture aux valeurs de x et y par le moyen d'une propriété X et Y respectivement
  • Donne accès en écriture aux valeurs de x et de y par le moyen des propriétés X et Y en validant que la valeur est valide pour le quadrant 1
  • Comporte un constructeur paramétrique qui s'assure que l'instance en création est dans un état valide

En cas d'erreur du programme client dans l'utilisation d'une instance de cette classe, la classe doit lever une exception :

  • Dans le cas où le code client tenterait de mettre une valeur négative en X, votre classe devra lever une exception de type CoordonnéeXInvalideException
  • Dans le cas où le code client tenterait de mettre une valeur négative en Y, votre classe devra lever une exception de type CoordonnéeYInvalideException

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la à l'aide du code client disponible sur le site Web du cours.

Exercice 2

Soit une classe représentant un compte bancaire (très simple). Les instances de cette classe devront offrir les services suivants :

  • Une propriété Solde permettant d'en connaître le solde
  • Une méthode Déposer permettant d'effectuer un dépôt
  • Une méthode Retirer permettant d'effectuer un retrait
  • Un constructeur par défaut
  • Un constructeur paramétrique permettant de préciser le solde initial du compte

Vous devez faire en sorte que par défaut, ce compte bancaire soit créé avec un solde à zéro; si le constructeur paramétrique est utilisé, la valeur précisée lors du processus d'instanciation doit être positive ou égale à 0.

Les méthodes permettant d'effectuer les dépôts et les retraits recevront en paramètre le montant à déposer ou à retirer selon le cas, qui doit être un entier positif strictement plus grand que 0, et ne retourneront rien.

Ce type de compte bancaire a un invariant : il ne pourra à aucun moment avoir un solde négatif. En vertu de l'encapsulation, vous devez vous assurer du respect de cet invariant, donc faire en sorte qu'en tout temps votre objet reste valide.

Dans le cas où le code client tenterait de créer une instance en y attribuant un solde négatif, votre classe devra lever une exception de type SoldeInvalideException.

Dans le cas où le code client tenterait de retirer un montant supérieur au solde du compte, ou encore de déposer un montant négatif ou nul, votre objet devra évidemment refuser l'opération et lever une exception de type OpérationInvalideException.

Travail à réaliser :

  • En précisant leur qualificateur d'accès, et en précisant s'il s'agit de membres d'instance (non-static) ou de membres de classe (static) :
    • Dressez la liste des attributs de cette classe
    • Dressez la liste des méthodes de cette classe
    • Dressez la liste des propriétés de cette classe
  • Rédigez cette classe et testez-la.

Une solution possible serait ceci : ClasseCompteBancaire.html

Exercice 3

Plus difficile, car moins directif. Vous développez un jeu impliquant des héros et des monstres. Les règles sont les suivantes :

  • Tout héros a des points de vie, représentés par un nombre entier
  • Tout héros a un nom
  • Tout monstre a des points de vie, représentés par un nombre entier
  • Tout monstre a un nom
  • Le nom d'un monstre doit être d'une longueur maximale de cinq caractères, et ne doit contenir que des consonnes
  • À la construction, un héros a un nombre de points de vie choisi aléatoirement entre 50 et 100 inclusivement
  • À la construction, un monstre a un nombre de points de vie indiqué par un paramètre passé au constructeur
  • Tout héros a une force, un entier dont la valeur est indiquée par un paramètre passé à la construction. Cette valeur doit être entre 10 et 20 inclusivement
  • Un héros peut frapper un autre personnage. Ceci blessera ce personnage en réduisant ses points de vie par une valeur pseudoaléatoire entre et de la force du héros
  • Tout monstre a une force, un entier dont la valeur est indiquée par un paramètre passé à la construction. Cette valeur doit être entre 15 et 25 inclusivement
  • Un monstre peut frapper un autre personnage. Ceci blessera ce personnage en réduisant ses points de vie par une valeur pseudoaléatoire entre et de la force du monstre
  • Un héros est un personnage
  • Un monstre est un personnage
  • Si les points de vie d'un personnage sont inférieurs ou égaux à zéro, alors ce personnage est mort, sinon il est vivant

Travail à réaliser :

  • Rédigez les classes Héros et Monstre
  • Identifiez ce qu'elles ont en commun et placez ces attributs, propriétés et méthodes dans une classe Personnage qui leur servira toutes deux de parent
  • Identifiez les attributs, propriétés, méthodes et constructeurs de chacune des classes que vous envisagez
  • Écrivez un petit programme de test représentant un combat entre un Héros et un Monstre et faisant la démonstration que votre design fonctionne, et respecte les consignes

Exercice 4

Sachant que la couleur de l'affichage de texte dans un écran console est donnée par la propriété Console.ForegroundColor, et sachant qu'il est possible de positionner le curseur où l'affichage de texte se fera à l'aide de la méthode Console.SetCursorPosition, écrivez une classe Carré telle que :

  • Un Carré a une position décrite par un point 2D
    • Note : à l'écran console, le point est le coin en haut et à gauche de l'écran, et les coordonnées en et en sont positives vers la droite et vers le bas respectivement
  • Un Carré a une longueur de côté
  • Un Carré a une couleur, de type ConsoleColor
  • Les caractéristiques d'une instance de Carré sont déterminées à la construction de cet objet
  • Dessiner un Carré dessinera ce carré à la position choisie, de la taille choisie, et à la couleur choisie

Travail à réaliser :

  • Rédigez la classe Carré
  • Identifiez les attributs, propriétés, méthodes et constructeurs de cette classe
  • Écrivez un programme de test dans lequel on trouvera un tableau de références sur des Carré et qui, en itérant à travers ce tableau, affichera ces divers objets à l'écran

9 février

S05

Au menu :

  • À venir
  • Remise du labo 00 – Le cryptographe

12 février

S06

Au menu :

  • À venir

16 février

S07

Au menu :

  • À venir

19 février

S08

Au menu :

  • À venir

23 février

S09

Au menu :

  • À venir

26 février

S10

Au menu :

  • À venir

2 mars

 

Jour de mise à niveau (cours suspendus)

5 mars

 

Jour de mise à niveau (cours suspendus)

9 mars

S11

Au menu :

  • À venir

12 mars

S12

Au menu :

  • À venir

16 mars

S13

Au menu :

  • À venir

19 mars

S14

Au menu :

  • À venir

23 mars

S15

Au menu :

  • À venir

26 mars

S16

Au menu :

  • À venir

30 mars

S17

Au menu :

  • À venir

2 avril

S18

Au menu :

  • À venir

6 avril

 

Jour férié (lundi de Pâques)

9 avril

S19

Au menu :

  • À venir

13 avril

S20

Au menu :

  • À venir

16 avril

S21

Au menu :

  • À venir

20 avril

S22

Au menu :

  • À venir

23 avril

S23

Au menu :

  • À venir

27 avril

S24

Au menu :

  • À venir

30 avril

S25

Au menu :

  • À venir

4 mai

S26

Au menu :

  • À venir

7 mai

s/o

Journée d'examen de français / formation générale (cours suspendus)

11 mai

S27

Au menu :

  • À venir

14 mai

S28

Au menu :

  • PFI

18 mai

s/o

Jour férié (Journée nationale des Patriotes)

19 mai

S29

Au menu :

  • Examen final

Attention : mardi selon l'horaire du lundi

Petits coups de pouces

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

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.

Travaux pratiques

Si vous cherchez les énoncés des travaux pratiques, vous pourrez entre autres les trouver ci-dessous (note : ce tableau est celui de H2021; je ne l'ai pas mis à jour pour H2024).

Travail Nom Rôle (probable)

Labo 00

À venir

Travail sur l'encapsulation et la conception de classes

Labo 01

À venir

Travail sur l'héritage et le polymorphisme

Labo 02

À venir

Travail sur les interfaces, l'encapsulation et le code générique

Labo 03

À venir

Travail sur les dictionnaires, les fonctions génériques et les entrées / sorties

PFI

Production finale d'intégration

À venir

Solutionnaires et exemples

Quelques solutionnaires suivent. En espérant que ça vous aide à organiser vos idées!

Programme de test – S04 Exercice 1a

L'exercice 1a de la séance S04 porte sur une classe Point générale dont chaque instance représente un point dans n'importe quel quadrant du plan cartésien. Un programme de test pour cet exercice suit.

//---------------------------------------------------------
// Programme de test de la classe Point
// 
// Ce programme vérifie que la classe Point fait 
// correctement son travail
//
// par Pierre Prud'homme, 2013 (retouché par Patrice Roy, 2020 puis 2023)
//---------------------------------------------------------
using System;

// test du constructeur par défaut
Point pDéfaut = new ();
AfficherPoint("Test du constructeur par défaut", pDéfaut);
AfficherSéparateur();

// test du constructeur paramétrique
Point pQuadrant1 = new (1.1f, 1.1f);
Point pQuadrant2 = new (-2.2f, 2.2f);
Point pQuadrant3 = new (-3.3f, -3.3f);
Point pQuadrant4 = new (4.4f, -4.4f);

AfficherPoint("Test du constructeur paramétrique pour Q1", pQuadrant1);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q2", pQuadrant2);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q3", pQuadrant3);
AfficherSéparateur();
AfficherPoint("Test du constructeur paramétrique pour Q4", pQuadrant4);
AfficherSéparateur();

// test du mutateur de x et de y
pQuadrant1.X = 10.10f;
pQuadrant1.Y = 10.10f;
AfficherPoint("Test du mutateur de Q1", pQuadrant1);
AfficherSéparateur();

TesterDistance();


// --------------------------
// fin du programme principal
// --------------------------


static bool AssezProches(float f0, float f1) => Math.Abs(f0 - f1) <= Math.Pow(10, -6);
static void TesterDistanceEx(Point p0, Point p1, float attendu)
{
   float dist = p0.Distance(p1);
   if (AssezProches(dist, attendu))
   {
      Console.WriteLine($"Distance entre [{p0.X},{p0.Y}] et [{p1.X},{p1.Y}] environ {attendu} comme prévu");
   }
   else
   {
      Console.WriteLine($"ERREUR : distance entre [{p0.X},{p0.Y}] et [{p1.X},{p1.Y}] == {dist}, mais {attendu} est attendu");
   }
}
static void TesterDistance()
{
   TesterDistanceEx(new Point(0,0), new Point(0,0), 0);
   TesterDistanceEx(new Point(0,0), new Point(1,0), 1);
   TesterDistanceEx(new Point(0,0), new Point(0,1), 1);
   TesterDistanceEx(new Point(0,0), new Point(1,1), (float) Math.Sqrt(2));
}

static void AfficherPoint(string message, Point p)
{
   Console.WriteLine($"{message} : le point vaut [{p.X}, {p.Y}]");
}
static string CréerSéparateur(int nbCar) => new string('-', nbCar);
static void AfficherSéparateur()
{
   const int NB_CAR_LIGNE = 72;
   Console.WriteLine($"\n{CréerSéparateur(NB_CAR_LIGNE)}\n");
}

Programme de test – S04 Exercice 1b

L'exercice 1b de la séance S04 porte sur une classe PointQuadrant1 générale dont chaque instance représente un point situé dans le premier quadrant du plan cartésien. Un programme de test pour cet exercice suit.

//---------------------------------------------------------
// Programme de test de la classe PointQuadrant1
// 
// Ce programme vérifie que la classe Point fait 
// correctement son travail
//
// par Pierre Prud'homme, 2013 (retouché par Patrice Roy, 2020 puis 2023)
//---------------------------------------------------------
using System;

TesterConstructeurParamétrique();
TesterMutateurs();


// --------------------------
// fin du programme principal
// --------------------------


static void AfficherPoint(string message, PointQuadrant1 p)
{
   Console.WriteLine($"{message} :");
   Console.WriteLine($"Le point vaut [{p.X}, {p.Y}]");
}
static string CréerSéparateur(int nbCar) => new string('-', nbCar);
static void AfficherSéparateur()
{
   const int NB_CAR_LIGNE = 72;
   Console.WriteLine($"\n{CréerSéparateur(NB_CAR_LIGNE)}\n");
}

static void TesterConstructeurParamétrique()
{
   // tests du constructeur paramétrique
   TesterPoint(1.1f, 1.1f);
   TesterPoint(-2.2f, 2.2f);
   TesterPoint(-3.3f, -3.3f);         
   TesterPoint(4.4f, -4.4f);
}

static void TesterPoint(float x, float y)
{
   Console.WriteLine($"Point reçu : [{x}, {y}]");
   try
   {
      PointQuadrant1 p = new (x, y);
      AfficherPoint("Test réussi du constructeur paramétrique pour PointQuadrant1", p);
   }
   catch (CoordonnéeXInvalideException)
   {
      Console.WriteLine("Point invalide en x lors de la construction");
   }
   catch (CoordonnéeYInvalideException)
   {
      Console.WriteLine("Point invalide en y lors de la construction");
   }
   AfficherSéparateur();
}

static void TesterMutateurs()
{
   PointQuadrant1 p = new (0, 0);

   ModifierPoint(p, 11.0f, p.Y);
   ModifierPoint(p, p.X, 11.0f);
   ModifierPoint(p, 111.0f, 111.0f);
   ModifierPoint(p, -22.2f, p.Y);
   ModifierPoint(p, p.X, -22.2f);
   ModifierPoint(p, -222.0f, -222.0f);
}

static void ModifierPoint(PointQuadrant1 p, float x, float y)
{
   Console.WriteLine($"Modification du point : [{x}, {y}]");
   try
   {
      p.X = x;
      p.Y = y;
      Console.WriteLine("Test réussi de la mutation du point");
   }
   catch (CoordonnéeXInvalideException)
   {
      Console.WriteLine("Point invalide en x lors de la modification");
   }
   catch (CoordonnéeYInvalideException)
   {
      Console.WriteLine("Point invalide en y lors de la modification");
   }
   AfficherPoint("Après mutation ", p);
   AfficherSéparateur();
}

Code vu en classe à la séance S05 pour l'exercice 3 de la séance S04

Le code vu en classe suit.

Classe Algos

Notez que cette classe est en évolution alors que la session progresse, et que nous la séparerons éventuellement en plusieurs classes.

using System;
namespace z
{
   class Algos
   {
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      //
      // Ceci est un cas raisonnable de double point de sortie pour une fonction :
      // sortir dès qu'on a trouvé la réponse permet d'économiser du temps, peut-être
      // même _beaucoup_ de temps
      //
      public static bool EstDans(char c, char [] cars)
      {
         foreach(char ch in cars)
            if(ch == c)
               return true;
         return false;
      }
      public static bool ContientSeulementConsonnnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) => EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) => char.IsAsciiLetter(c) && !EstVoyelle(c);
   }
}

Classe Personnage

using System;
namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }
   class Personnage
   {
      public int Vie { get; private set; }
      public bool EstMort => Vie <= 0;
      public bool EstVivant => !EstMort;
      public string Nom { get; private init; }
      public int Force { get; private init; }
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
   }
}

Classe Héros

using System;
namespace z
{
   class Héros : Personnage
   {
      public Héros(string nom, int force)
         : base(nom, GénérerVieInitiale(), ValiderForce(force))
      {
      }
      static int GénérerVieInitiale()
      {
         const int VIE_MIN = 50,
                   VIE_MAX = 100;
         return new Random().Next(VIE_MIN, VIE_MAX + 1); // bof
      }
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
   }
}

Classe Monstre

using System;
namespace z
{
   class Monstre : Personnage
   {
      public Monstre(string nom, int vie, int force)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      const int LG_MAX_NOM = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_MAX_NOM &&
         Algos.ContientSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ? candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
   }
}

Code (complet cette fois) vu en classe à la séance S06 pour l'exercice 3 de la séance S04

Le code vu en classe suit.

Classe Algos

Notez que cette classe est en évolution alors que la session progresse, et que nous la séparerons éventuellement en plusieurs classes. Depuis la version précédente, rien n'a changé.

using System;
namespace z
{
   class Algos
   {
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      //
      // Ceci est un cas raisonnable de double point de sortie pour une fonction :
      // sortir dès qu'on a trouvé la réponse permet d'économiser du temps, peut-être
      // même _beaucoup_ de temps
      //
      public static bool EstDans(char c, char [] cars)
      {
         foreach(char ch in cars)
            if(ch == c)
               return true;
         return false;
      }
      public static bool ContientSeulementConsonnnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) => EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) => char.IsAsciiLetter(c) && !EstVoyelle(c);
   }
}

Classe Personnage

Depuis la version précédente, nous avons ajouté la méthode Frapper (notez la qualification protected). Nous avons choisi d'offrir une fonction générale à deux paramètres qui pourra être appelée par d'autres de manière à préserver le côté privé du mutateur de Vie.

using System;
namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }
   class Personnage
   {
      public int Vie { get; private set; }
      public bool EstMort => Vie <= 0;
      public bool EstVivant => !EstMort;
      public string Nom { get; private init; }
      public int Force { get; private init; }
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
      protected void Frapper(Personnage autre, int dégâts)
      {
         autre.Vie -= dégâts;
      }
   }
}

Classe Héros

Depuis la version précédente, nous avons ajouté la méthode Frapper. Nous avons choisi d'offrir une fonction spécialisée à un paramètre qui déléguera le travail plus général vers la méthode à deux paramètres du parent.

using System;
namespace z
{
   class Héros : Personnage
   {
      public Héros(string nom, int force)
         : base(nom, GénérerVieInitiale(), ValiderForce(force))
      {
      }
      static int GénérerVieInitiale()
      {
         const int VIE_MIN = 50,
                   VIE_MAX = 100;
         return new Random().Next(VIE_MIN, VIE_MAX + 1); // bof
      }
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
      public void Frapper(Personnage autre)
      {
         const int PCT_MIN = 50,
                   PCT_MAX = 100;
         int dégâts = Force * new Random().Next(PCT_MIN, PCT_MAX + 1) / 100;
         Frapper(autre, dégâts);
      }
   }
}

Classe Monstre

Depuis la version précédente, nous avons ajouté la méthode Frapper. Nous avons choisi d'offrir une fonction spécialisée à un paramètre qui déléguera le travail plus général vers la méthode à deux paramètres du parent.

using System;
namespace z
{
   class Monstre : Personnage
   {
      public Monstre(string nom, int vie, int force)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      const int LG_MAX_NOM = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_MAX_NOM &&
         Algos.ContientSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ? candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         Algos.EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ? candidate : throw new ForceInvalideException();
      public void Frapper(Personnage autre)
      {
         const int PCT_MIN = 50,
                   PCT_MAX = 75;
         int dégâts = Force * new Random().Next(PCT_MIN, PCT_MAX + 1) / 100;
         Frapper(autre, dégâts);
      }
   }
}

Programme de test

Le code de notre programme de test suit.

using System;
Héros héros = new ("Adam", 18);
Monstre monstre = new ("GRR", 75, 20);
Tour àQui = ChoisirProtagoniste();
while(héros.EstVivant && monstre.EstVivant)
{
   Console.WriteLine($"Avant le choc, {héros.Nom} a {héros.Vie} vie");
   Console.WriteLine($"Avant le choc, {monstre.Nom} a {monstre.Vie} vie");
   if(àQui == Tour.héros)
   {
      Console.WriteLine($"{héros.Nom} frappe {monstre.Nom}");
      héros.Frapper(monstre);
      àQui = Tour.monstre;
   }
   else
   {
      Console.WriteLine($"{monstre.Nom} frappe {héros.Nom}");
      monstre.Frapper(héros);
      àQui = Tour.héros;
   }
   Console.WriteLine($"Après le choc, {héros.Nom} a {héros.Vie} vie");
   Console.WriteLine($"Après le choc, {monstre.Nom} a {monstre.Vie} vie");
   Console.WriteLine(new string('-', 60));
}
if (héros.EstMort)
   Console.WriteLine($"{héros.Nom} est mort... snif!");
if (monstre.EstMort)
   Console.WriteLine($"{monstre.Nom} est mort... ouf!");

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

static Tour ChoisirProtagoniste() =>
   new Random().Next() % 2 == 0? Tour.héros : Tour.monstre;
enum Tour { héros, monstre }

Code vu en classe à la séance S06 pour l'exercice 4 de la séance S04

Le code vu en classe suit.

Classe Point

using System;
namespace S04ex04
{
   class Point
   {
      int x;
      int y;
      static bool EstXValide(int x) => x >= 0;
      static bool EstYValide(int y) => y >= 0;
      public int X
      {
         get => x;
         set
         {
            x = EstXValide(value) ? value : throw new ArgumentException(); 
         }
      }
      public int Y
      {
         get => y;
         set
         {
            y = EstYValide(value) ? value : throw new ArgumentException();
         }
      }

      public Point(int x, int y)
      {
         X = x;
         Y = y;
      }
      public Point() : this(0,0)
      {
      }
   }
}

Classe Carré

using System;
namespace S04ex04
{
   class Carré
   {
      public Point Position
      {
         get; private init;
      }
      public int Côté
      {
         get; private init;
      }
      public ConsoleColor Couleur
      {
         get; private init;
      }
      public Carré(Point pos, int côté, ConsoleColor couleur)
      {
         Position = pos;
         Côté = côté;
         Couleur = couleur;
      }
      public void Dessiner()
      {
         ConsoleColor avant = Console.ForegroundColor;
         Console.ForegroundColor = Couleur;
         for (int ligne = 0; ligne != Côté; ++ligne)
         {
            for(int col = 0; col != Côté; ++col)
            {
               Console.SetCursorPosition(Position.X + col, Position.Y + ligne);
               Console.Write('#');
            }
         }
         Console.ForegroundColor = avant;
      }
   }
}

Code de test

using System;

Carré[] carrés = new Carré[]
{
   new Carré(new Point(2, 5), 5, ConsoleColor.Blue),
   new Carré(new Point(15, 8), 3, ConsoleColor.Green),
   new Carré(new Point(7, 12), 6, ConsoleColor.Yellow)
};
Random r = new ();
for(int i = 0; i != 10; ++i)
{
   Console.Clear();
   foreach (Carré c in carrés)
      c.Dessiner();
   System.Threading.Thread.Sleep(1000);
   foreach (Carré c in carrés)
   {
      try
      {
         c.Position.X += r.Next(0, 5) - 2;
         c.Position.Y += r.Next(0, 5) - 2;
      }
      catch(ArgumentException)
      {
      }
   }
}

Code vu en classe lors de la séance S15

Nous avons fait trois petites structures de données dans ce cours, soit :

Classe Tableau

Le code de la classe suit :

using System;
using System.Collections.Generic;
using System.Diagnostics.SymbolStore;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace z
{
   class Tableau
   {
      int[] Éléments { get; set; }
      public int Count { get; private set; } = 0;
      public int Capacity => Éléments.Length;
      public bool EstVide => Count == 0;
      public bool EstPlein => Count == Capacity;
      public Tableau()
      {
         Éléments = new int[0]; // ou Éléments = null
      }
      public void Add(int valeur)
      {
         if (EstPlein)
            Croître();
         Éléments[Count] = valeur;
         ++Count;
      }
      private void Croître()
      {
         int nouvelleCap = Capacity == 0? 16 : Capacity * 2;
         int[] nouvTab = new int[nouvelleCap];
         for (int i = 0; i != Count; ++i)
            nouvTab[i] = Éléments[i];
         Éléments = nouvTab;
      }
      public int Find(int valeur)
      {
         for (int i = 0; i != Count; ++i)
            if (valeur.Equals(Éléments[i]))
               return i;
         return -1;
      }
      public void RemoveAt(int indice)
      {
         if(indice >= Count)
            throw new IndexOutOfRangeException();
         for (; indice < Count - 1; ++indice)
            Éléments[indice] = Éléments[indice + 1];
         --Count;
      }
      public void Remove(int valeur)
      {
         int indice = Find(valeur);
         if(indice != -1)
            RemoveAt(indice);
      }
      public int At(int indice)
      {
         return Éléments[indice];
      }
   }
}

Classe Pile (version reposant sur un Tableau)

Le code de la classe suit (je ne répète pas la classe Tableau par souci d'économie) :

class PileVideException : Exception { }
class Pile
{
   Tableau Substrat { get; init; }
   public Pile()
   {
      Substrat = new();
   }
   public bool EstVide { get => Substrat.EstVide; }
   public void Push(int valeur) // en français : Empiler
   {
      Substrat.Add(valeur);
   }
   public int Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Substrat.At(Substrat.Count - 1);
   }
   public int Pop()
   {
      int valeur = Peek();
      Substrat.RemoveAt(Substrat.Count - 1);
      return valeur;
   }
}

Classe Pile (version reposant sur des noeuds)

Le code de la classe suit :

class PileVideException : Exception { }
class Pile
{
   class Noeud
   {
      public int Valeur { get; init; }
      public Noeud Prédécesseur { get; set; }
      public Noeud(int valeur)
      {
         Valeur = valeur;
         Prédécesseur = null;
      }
   }
   Noeud Tête { get; set; }
   public Pile()
   {
      Tête = null;
   }
   public bool EstVide { get => Tête == null; }
   public void Push(int valeur) // en français : Empiler
   {
      Noeud p = new(valeur);
      p.Prédécesseur = Tête;
      Tête = p;
   }
   public int Peek()
   {
      if (EstVide)
         throw new PileVideException();
      return Tête.Valeur;
   }
   public int Pop()
   {
      int valeur = Peek();
      Tête = Tête.Prédécesseur;
      return valeur;
   }
}

Activité de la séance S13

Un exemple de code implémentant une solution à ce problème suit.

Classe Algos

namespace z
{
   internal static class Algos
   {
      static char[] voyelles = { 'a', 'e', 'i', 'o', 'u', 'y' };
      static bool EstDans(char c, char[] vals)
      {
         foreach(char ch in vals)
            if (ch == c)
               return true;
         return false;
      }
      public static bool EstSeulementConsonnes(string s)
      {
         foreach(char c in s)
            if(!EstConsonne(c))
               return false;
         return true;
      }
      public static bool EstVoyelle(char c) =>
         EstDans(char.ToLower(c), voyelles);
      public static bool EstConsonne(char c) =>
         char.IsAsciiLetter(c) && !EstVoyelle(c);
      public static bool EstEntreInclusif(int val, int min, int max) =>
         min <= val && val <= max;
   }
}

Classe Arme

namespace ActivitéS13
{
   internal class Arme
   {
      public string Nom { get; init; }
      public int Effet { get; init; }
      public Arme(string nom, int effet)
      {
         Nom = nom;
         Effet = effet;
      }
      public int AppliquerEffet(int dégâts) => dégâts + Effet;
   }
}

Classe Personnage

namespace z
{
   class ForceInvalideException : Exception { }
   class NomInvalideException : Exception { }

   internal class Personnage
   {
      public string Nom { get; private init; }
      public int Vie { get; private set; }
      public int Force { get; private init; }
      public bool EstVivant => !EstMort;
      public bool EstMort => Vie <= 0;
      public Personnage(string nom, int vie, int force)
      {
         Nom = nom;
         Vie = vie;
         Force = force;
      }
      protected static void Frapper(Personnage autre, int dégâts)
      {
         autre.Vie -= dégâts;
      }
      public virtual void Frapper(Personnage autre) // ICI
      {
         Frapper(autre, 1); // bof (on fera mieux bientôt)
      }
   }
}

Classe Héros

using static z.Algos;

namespace z
{
   internal class Héros : Personnage
   {
      const int FORCE_MIN = 10,
                FORCE_MAX = 20;
      static bool EstForceValide(int candidate) =>
         EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ?
            candidate : throw new ForceInvalideException();
      // bornes pour la valeur initiale seulement
      const int VIE_MIN = 50,
                VIE_MAX = 100;
      public Héros(string nom, int force)
         : this(nom, force, null)
      {
      }
      public Héros(string nom, int force, Arme arme)
         : base(nom, new Random().Next(VIE_MIN, VIE_MAX + 1),
          ValiderForce(force))
      {
         ZeArme = arme;
      }
      public override void Frapper(Personnage p) // ICI
      {
         const int PCT_DÉGÂTS_MIN = 50,
                   PCT_DÉGÂTS_MAX = 100;
         int dégâts =
            (int) (Force *
                   (new Random().Next(PCT_DÉGÂTS_MIN, PCT_DÉGÂTS_MAX + 1) /
                   100.0));
         Frapper
         (
            p, ZeArme == null? dégâts : ZeArme.AppliquerEffet(dégâts)
         );
      }
      public Arme ZeArme { get; init; }
   }
}

Classe Monstre

using static z.Algos;

namespace z
{
   internal class Monstre : Personnage
   {
      const int LG_NOM_MAX = 5;
      static bool EstNomValide(string candidat) =>
         candidat.Length <= LG_NOM_MAX &&
         Algos.EstSeulementConsonnes(candidat);
      static string ValiderNom(string candidat) =>
         EstNomValide(candidat) ?
            candidat : throw new NomInvalideException();
      const int FORCE_MIN = 15,
                FORCE_MAX = 25;
      static bool EstForceValide(int candidate) =>
         EstEntreInclusif(candidate, FORCE_MIN, FORCE_MAX);
      static int ValiderForce(int candidate) =>
         EstForceValide(candidate) ?
            candidate : throw new ForceInvalideException();
      public Monstre(string nom, int force, int vie)
         : base(ValiderNom(nom), vie, ValiderForce(force))
      {
      }
      public override void Frapper(Personnage p) // ICI
      {
         const int PCT_DÉGÂTS_MIN = 50,
                   PCT_DÉGÂTS_MAX = 75;
         int dégâts =
            (int)(Force *
                   (new Random().Next(PCT_DÉGÂTS_MIN, PCT_DÉGÂTS_MAX + 1) /
                   100.0));
         Frapper(p, dégâts);
      }
   }
}

Classe GroupePersos

namespace ActivitéS13
{
   internal class GroupePersos
   {
      Random Dé { get; init; } = new();
      Personnage [] Persos { get; init; }
      protected GroupePersos(Personnage[] persos)
      {
         Persos = persos; // pas bon, mais on s'en reparle
      }
      protected int NbÉléments => Persos.Length;
      protected int CompterMorts()
      {
         int n = 0;
         foreach (Personnage p in Persos)
            if (p.EstMort)
               ++n;
         return n;
      }
      public void Attaquer(GroupePersos autres)
      {
         Personnage deQui = Persos[Dé.Next(NbÉléments)];
         while(deQui.EstMort) // peut être long...
            deQui = Persos[Dé.Next(NbÉléments)];
         Personnage versQui = autres.Persos[Dé.Next(autres.NbÉléments)];
         while(versQui.EstMort) // peut être long...
            versQui = autres.Persos[Dé.Next(autres.NbÉléments)];
         Console.WriteLine($"{deQui.Nom} frappe {versQui.Nom}");
         Console.WriteLine($"\tAvant le coup, {versQui.Nom} a {versQui.Vie} vie");
         deQui.Frapper(versQui);
         Console.WriteLine($"\tAprès le coup, {versQui.Nom} a {versQui.Vie} vie");

      }
      public bool EstDéfait =>
            CompterMorts() == NbÉléments;
      public void PrésenterVivants()
      {
         foreach (Personnage p in Persos)
            if (p.EstVivant)
               Console.Write($"{p.Nom} avec {p.Vie} vies; ");
      }
   }
}

Classe Armée

namespace ActivitéS13
{
   class ArméeMalforméeException : Exception { }
   internal class Armée : GroupePersos
   {
      static bool EstArméeValide(Héros[] h) =>
         h.Length != 0 && h.Length % 2 == 0;
      static Héros[] ValiderArmée(Héros[] h) =>
         EstArméeValide(h)? h : throw new ArméeMalforméeException();
      public Armée(Héros[] héros) : base(ValiderArmée(héros))
      {
      }
   }
}

Classe Horde

namespace ActivitéS13
{
   internal class Horde : GroupePersos
   {
      class HordeMalforméeException : Exception { }
      static bool EstHordeValide(Monstre[] m) => m.Length != 0;
      static Monstre[] ValiderHorde(Monstre[] m) =>
         EstHordeValide(m) ? m : throw new HordeMalforméeException();
      public Horde(Monstre[] monstres) : base(ValiderHorde(monstres))
      {
      }
   }
}

Classe Program

using ActivitéS13;
using z;

Armée armée = new(new Héros[]
{
   new Héros("Bill le petit", 15),
   new ("Valentin", 18),
   new ("Galahad", 17, new Arme("Halebarde", 8)),
   new ("Brute immonde", 20, new Arme("Gourdin", 3))
});
Horde horde = new(new Monstre[]
{
   new("GRRR", 24, 50),
   new("HSSSS", 22, 44),
   new("BRK", 20, 40),
   new("FFFT", 19, 15),
   new("prkkk", 21, 38)
});

bool tourHéros = new Random().Next() % 2 == 0;
while (!armée.EstDéfait && !horde.EstDéfait)
{
   if (tourHéros)
   {
      armée.Attaquer(horde);
   }
   else
   {
      horde.Attaquer(armée);
   }
   Console.WriteLine(new string('-', 70));
   tourHéros = !tourHéros;
}
//Console.WriteLine($"Victoire de {(héros.EstVivant? héros.Nom : monstre.Nom)}");
if (horde.EstDéfait)
{
   Console.Write("Victoire de l'armée. Encore vivants : ");
   armée.PrésenterVivants();
}
else
{
   Console.Write("Victoire de la horde. Encore vivants : ");
   horde.PrésenterVivants();
}

 


Valid XHTML 1.0 Transitional

CSS Valide !