Navigation globale
Public MustInherit Class Carte ''' <summary> ''' Constructeur protégé, seules les classes filles pourront l'appeler ''' </summary> ''' <param name="CheminDeLimage"></param> ''' <param name="LesPoints"></param> Protected Sub New(ByVal CheminDeLimage As String, ByVal LesPoints As Integer) CheminImage = CheminDeLimage Points = LesPoints End Sub ''' <summary> ''' Chemin vers l'image de la face ''' </summary> ''' <remarks>C'est la couche Vue qui s'occupera de l'afficher</remarks> Private privateCheminImage As String Public Property CheminImage As String Get Return privateCheminImage End Get Private Set(ByVal value As String) privateCheminImage = value End Set End Property ''' <summary> ''' Nombre de points marqués avec cette carte ''' </summary> Private privatePoints As Integer Public Property Points As Integer Get Return privatePoints End Get Protected Set(ByVal value As Integer) privatePoints = value End Set End Property Protected leJoueur As Joueur ''' <summary> ''' Prend la première carte de la pioche et la main dans la main d'un joueur ''' </summary> ''' <param name="LeJoueur"></param> Public Sub Piocher(ByVal LeJoueur As Joueur) Me.leJoueur = LeJoueur 'ToDo implémenter la pioche End Sub ''' <summary> ''' Joue la carte ''' </summary> Protected Sub Jouer() 'ToDo implémenter le jeu End Sub ''' <summary> ''' Jette une carte de la main du joueur vers la défausse ''' </summary> Public Sub Defausse() 'ToDo implémenter la défausse End Sub End Class
Comment ça
'ToDo implémenter la défausse
? C’est simple, nous n’avons pas encore défini ce qu’est un Joueur, la défausse et la pioche.
D’ailleurs, juste pour que ça compile
Class Joueur End Class
Public Class Kilometre Inherits Carte Public Sub New(ByVal CheminDeLimage As String, ByVal LesKilometres As Integer) 'on appelle le constructeur de Carte MyBase.New(CheminDeLimage, LesKilometres) Distance = LesKilometres End Sub Private privateDistance As Integer Public Property Distance As Integer Get Return privateDistance End Get Private Set(ByVal value As Integer) privateDistance = value End Set End Property Public Shadows Sub Jouer() 'A noter le modificateur Shadows qui permet de masquer la méthode héritée MyBase.Jouer() 'appel du code de la classe mère End Sub ''' <summary> ''' Réécriture de ToString pour afficher le nombre de kilomètres ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Return Distance.ToString & " km" End Function End Class Public Class Embuche Inherits Carte Public Sub New(ByVal CheminImage As String, ByVal LeType As TypeEmbuche) MyBase.New(CheminImage,0) Type = LeType End Sub Private privateType As TypeEmbuche Public Property Type As TypeEmbuche Get Return privateType End Get Private Set(ByVal value As TypeEmbuche) privateType = value End Set End Property ''' <summary> ''' Une embuche est lancé à un autre joueur ''' </summary> ''' <param name="Cible"></param> Public Sub Jouer(ByVal Cible As Joueur) 'ToDo implémenter le jeu de l'embuche End Sub ''' <summary> ''' Réécriture de ToString pour afficher le nom de l'embuche ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Return Type.ToString End Function End Class ''' <summary> ''' Enumération permettant de distinguer le type de chaque carte Embuche ''' </summary> Public Enum TypeEmbuche FeuRouge LimitationVitesse Crevaison PanneEssence Accident End Enum Public Class Parade Inherits Carte Public Sub New(ByVal CheminImage As String, ByVal LeType As TypeEmbuche) MyBase.New(CheminImage, 0) EmbucheParee = LeType End Sub Private privateEmbucheParee As TypeEmbuche Public Property EmbucheParee As TypeEmbuche Get Return privateEmbucheParee End Get Private Set(ByVal value As TypeEmbuche) privateEmbucheParee = value End Set End Property ''' <summary> ''' Réécriture de ToString pour afficher le nom de la parade ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Select Case EmbucheParee Case TypeEmbuche.Accident Return "Réparation" Case TypeEmbuche.Crevaison Return "Roue de secours" Case TypeEmbuche.FeuRouge Return "Feu vert" Case TypeEmbuche.LimitationVitesse Return "Fin de limitation" Case TypeEmbuche.PanneEssence Return "Station essence" Case Else 'on est obligé de mettre un Case Else ou après le Select Case return quelque chose car ToString retourne un résultat, et même si on a testé toutes les options de TypeEmbuche, le compilateur considère qu'un cas non prévu pourrait se présenter et qu'il n'y aurait alors pas de résultat Return "Erreur" End Select End Function End Class Public Class Botte Inherits Carte Public Sub New(ByVal CheminImage As String, ByVal LeType As TypeEmbuche) MyBase.New(CheminImage, 100) EmbucheImmunisee = LeType End Sub Private privateEmbucheImmunisee As TypeEmbuche Public Property EmbucheImmunisee As TypeEmbuche Get Return privateEmbucheImmunisee End Get Private Set(ByVal value As TypeEmbuche) privateEmbucheImmunisee = value End Set End Property ''' <summary> ''' Cette méthode est quand la carte se joue elle-même ''' </summary> Public Sub CoupFourre() Me.Points += 300 Piocher(Me.leJoueur) 'ToDo implémenter le fait que ce soit au tour de ce joueur. End Sub ''' <summary> ''' Réécriture de ToString pour afficher le nom de la botte ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Select Case EmbucheImmunisee Case TypeEmbuche.Accident Return "As du volant" Case TypeEmbuche.Crevaison Return "Roue increvable" Case TypeEmbuche.FeuRouge, TypeEmbuche.LimitationVitesse Return "Camion de pompier" Case TypeEmbuche.PanneEssence Return "Citerne d'essence" End Select 'on est obligé de mettre un Case Else ou après le Select Case return quelque chose car ToString retourne un résultat, et même si on a testé toutes les options de TypeEmbuche, le compilateur considère qu'un cas non prévu pourrait se présenter et qu'il n'y aurait alors pas de résultat Return "Erreur" End Function End Class
OK, mais il nous reste plein de code à définir, pour cela il nous faut savoir ce que sont le Joueur et le Moteur du jeu.
Un joueur a comme propriétés :
Le joueur peut
Pour cette implémentation, je vais intentionnellement dupliquer le code de ces 3 méthodes pour montrer que les 2 options étaient possibles.
Le Joueur émet un évènement quand
Le constructeur permet d’affecter le nom.
Note : la gestion du coup fourré nécessite de modifier le nombre de points de la botte, or celui-ci est en lecture seule, on peut soit rendre public tous les points ou ceux des bottes, soit ajouter une méthode signalant un coup fourré. Je choisi la dernière option.
La pioche et la défausse étant les mêmes pour tout le monde, on peut être tenté par des variables globales. Cette pratique est considérée comme mauvaise en .Net.
Pour suivre cette recommandation, nous avons le choix entre passer en paramètre des méthodes concernées la pioche et la défausse, ou les inclure dans une classe particulière : la classe partagée.
Le mot clé est
Shared
, se traduisant par partagé, j’utilise donc le terme classe partagée.
Une classe partagée est une classe qui ne peut pas être instanciée, mais peut être utilisée (contrairement à la classe abstraite).
Un exemple simple, la classe Math on ne peut pas instancier cette classe, et pourtant on peut calculer un cosinus, une valeur absolue, etc…
cos = Math.Cos(10)
On constate que Cos() est une méthode, mais contrairement à celles que l’on a déjà vues, elle est « rattachée » à la classe et pas à une instance. On dit qu’il s’agit d’une méthode de classe.
De même, Math nous donne accès à π
pi = Math.PI
PI est une propriété de classe
Une classe partagée ne dispose que de méthode(s), propriété(s) et événement(s) de classe.
Une classe « normale », peut disposer de méthode(s), propriété(s) et événement(s) de classe.
La classe Moteur est une classe partagée
Il a comme propriétés
Il doit
Pour initialiser les cartes, je pourrais tout à fait écrire une méthode dans le moteur dont ce serait le rôle.
Cependant, je vais vous montrer une méthode de classe avec une classe instanciable. Chaque classe fille aura sa propre méthode de génération.
Le moteur fera juste appel à cette méthode pour les 4 classe filles.
Public Class Joueur Public Delegate Sub EvenementKilometres(ByVal LeJoueur As Joueur, ByVal Kilometres As Integer) 'signature de l'évènement Public Event JaiGagne As EvenementKilometres 'évènement signalant que la distance est atteinte Public Event DistanceDepassee As EvenementKilometres 'évènement signalant que la distance est partagée Public Delegate Sub EvenementEmbuche(ByVal LeJoueur As Joueur) 'signature de l'évènement Public Event EmbucheRecue As EvenementEmbuche 'évènement signalant une embuche reçue Public Event CoupFourre As EvenementEmbuche 'évènement signalant un coup fourré Public Event FiniMonTour As EvenementEmbuche 'évènement signalant que le joueur à fini son tour. Public Sub New(ByVal LeNom As String) Nom = LeNom DistanceParcourue = 0 Main = New List(Of Carte) Jeu = New ObservableCollection(Of Carte) 'initialisation de la collection AddHandler Jeu.CollectionChanged, AddressOf Jeu_CollectionChanged 'Abonnement à l'évènement signalant un changement End Sub Private privateNom As String Public Property Nom As String Get Return privateNom End Get Private Set(ByVal value As String) privateNom = value End Set End Property Public Property Main As List(Of Carte) Public Property Jeu As ObservableCollection(Of Carte) ''' <summary> ''' Propriété en lecture seule, dont la valeur est le résultat d'un calcul ''' </summary> Public ReadOnly Property Points As Integer Get Dim bonus As Integer = 0 'si un (ou plusieurs) joueur n'a pas pu poser de kilomètre, les autres marquent 500 points de bonus For Each leJoueur As Joueur In Moteur.Joueurs If leJoueur Is Me Then Continue For 'on passe à l'itération suivante End If If leJoueur.DistanceParcourue = 0 Then bonus += 500 'on ajoute 500 au bonus pour se joueur sans distance End If Next leJoueur Dim lesPoints As Integer = 0 For Each laCarte As Carte In Jeu lesPoints += laCarte.Points 'grâce au polymorphisme, on peut additionner les points, peu importe le type de carte. Next laCarte Return lesPoints + bonus End Get End Property Private privateDistanceParcourue As Integer Public Property DistanceParcourue As Integer Get Return privateDistanceParcourue End Get Private Set(ByVal value As Integer) privateDistanceParcourue = value End Set End Property ''' <summary> ''' Traitements à faire après qu'une carte ait été ajoutée au jeu ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> Private Sub Jeu_CollectionChanged(ByVal sender As Object, ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs) If e.Action <> System.Collections.Specialized.NotifyCollectionChangedAction.Add Then Return End If If TypeOf Jeu.Last Is Embuche Then 'si la dernière carte reçue est une embuche, car Jeu contient des Cartes RaiseEvent EmbucheRecue(Me) 'on émet l'évènement Dim lEmbuche As Embuche = CType(Jeu.Last, Embuche) 'pour travailler sur une Embuche, à partir d'une Carte, il faut "caster" (forcer la conversion) la Carte en Embuche 'on regarde si le joueur dispose de la botte pour faire un coup fourré For Each laCarte As Carte In Main If TypeOf laCarte Is Botte AndAlso (CType(laCarte, Botte)).EmbucheImmunisee = lEmbuche.Type Then Dim laBotte As Botte = CType(laCarte, Botte) 'on signale que l'on joue le coup fourré RaiseEvent CoupFourre(Me) laBotte.CoupFourreParLeJoueur() 'appelle la méthode qui va mettre le bonus de points Jouer(laBotte) 'On joue la botte Jeu.Remove(lEmbuche) 'on enlève l'embuche du jeu, ce qui va regénérer l'évènement Jeu_CollectionChanged, d'ou le premier test Moteur.Defausse.Add(lEmbuche) 'on jette l'embuche à la défausse Pioche() 'on pioche pour remplacer la botte Exit For 'on sort du foreach, ça n'est pas la peine de continuer à cherche ce que l'on a déjà trouvé End If Next laCarte ElseIf TypeOf Jeu.Last Is Kilometre Then 'si la dernière carte est un kilomètre, on calcule la distance totale. Dim km As Kilometre = CType(Jeu.Last, Kilometre) DistanceParcourue = km + DistanceParcourue If DistanceParcourue = Moteur.DistanceAParcourir Then RaiseEvent JaiGagne(Me,DistanceParcourue) 'on émet l'évènement avec le paramètre correspondant à la signature. ElseIf DistanceParcourue > Moteur.DistanceAParcourir Then RaiseEvent DistanceDepassee(Me,DistanceParcourue) End If End If End Sub ''' <summary> ''' Méthode permettant de jouer une Carte dans son jeu ''' </summary> ''' <param name="LaCarte"></param> ''' <remarks>Si cette méthode était publique, on se jouerait une embuche à soi-même</remarks> Private Sub Joue(ByVal LaCarte As Carte) Main.Remove(LaCarte) 'On enlève la carte de la main Jeu.Add(LaCarte) 'on ajoute la carte au jeu AnnonceFinTour() End Sub ''' <summary> ''' Méthode permettant de jouer un kilomètre ''' </summary> ''' <param name="Km"></param> ''' <remarks>Je crée 3 surcharges pour être sûr qu'une embuche ne sera pas transmise en paramètre</remarks> Public Sub Jouer(ByVal Km As Kilometre) Joue(CType(Km, Carte)) End Sub ''' <summary> ''' Surcharge permettant de jouer une parade ''' </summary> ''' <param name="LaParade"></param> Public Sub Jouer(ByVal LaParade As Parade) Joue(CType(LaParade, Carte)) End Sub ''' <summary> ''' Surcharge permettant de jouer une botte ''' </summary> ''' <param name="LaParade"></param> Public Sub Jouer(ByVal LaBotte As Botte) Joue(CType(LaBotte, Carte)) End Sub ''' <summary> ''' Surcharge permettant de mettre une embuche à un adversaire ''' </summary> ''' <param name="Lembuche"></param> ''' <param name="Cible"></param> Public Sub Jouer(ByVal Lembuche As Embuche, ByVal Cible As Joueur) If Cible Is Me Then 'si le joueur cible est le joueur en cours, on génère une erreur Throw New Exception("Le joueur ne peut pas se mettre une embuche à lui-même") Else Main.Remove(Lembuche) 'On enlève la carte de la main Cible.Jeu.Add(Lembuche) 'on ajoute la carte au jeu du joueur cible End If AnnonceFinTour() End Sub ''' <summary> ''' Pioche une carte ''' </summary> Public Sub Pioche() Main.Add(Moteur.Pioche(0)) 'pioche la première carte Moteur.Pioche.RemoveAt(0) End Sub ''' <summary> ''' Défausse une carte ''' </summary> ''' <param name="LaCarte">Carte à défausser</param> Public Sub Defausse(ByVal LaCarte As Carte) Moteur.Defausse.Add(LaCarte) Main.Remove(LaCarte) AnnonceFinTour() End Sub ''' <summary> ''' Annonce que le tour est terminé ''' </summary> Private Sub AnnonceFinTour() RaiseEvent FiniMonTour(Me) End Sub End Class
Public NotInheritable Class Moteur Public Delegate Sub EvenementProchainJoueur(ByVal LeJoueur As Joueur) 'signature de l'évènement Public Shared Event ProchainJoueur As EvenementProchainJoueur 'évènement annonçant le joueur suivant Public Shared Event PartieGagnee As EvenementProchainJoueur 'évènement annonçant qui a gagné Public Delegate Sub EvenementMoteurJeu() 'signature de l'évènement Public Shared Event NouvellePioche As EvenementMoteurJeu 'évènement annonçant la nouvelle pioche Public Shared Property DistanceAParcourir As Integer Public Shared Property Joueurs As List(Of Joueur) Public Shared Property Defausse As List(Of Carte) Public Shared Property Pioche As ObservableCollection(Of Carte) Private Sub New() End Sub Public Shared ReadOnly Property JoueurEnCours As Joueur Get Return Joueurs(indexJoueur) End Get End Property Private Shared indexJoueur As Integer ''' <summary> ''' Initialise les éléments de jeu avant la première partie ''' </summary> ''' <param name="NomsJoueurs"></param> Public Shared Sub InitPremierePartie(ByVal NomsJoueurs As List(Of String)) Defausse = New List(Of Carte) Pioche = New ObservableCollection(Of Carte) AddHandler Pioche.CollectionChanged, AddressOf Pioche_CollectionChanged Joueurs = New List(Of Joueur) 'Crée les joueurs et s'abonne aux évènements For Each joueur As String In NomsJoueurs Dim j As New Joueur(joueur) AddHandler j.CoupFourre, AddressOf j_CoupFourre AddHandler j.DistanceDepassee, AddressOf j_DistanceDepassee AddHandler j.JaiGagne, AddressOf j_JaiGagne AddHandler j.FiniMonTour, AddressOf j_FiniMonTour Joueurs.Add(j) Next joueur DistanceAParcourir = If(Joueurs.Count > 3, 700, 1000) 'La distance à parcourir est 700 à partir de 4 joueurs, sinon 1000 NouvellePartie() End Sub ''' <summary> ''' Quand un joueur à finit, on passe au suivant ''' </summary> ''' <param name="LeJoueur"></param> Private Shared Sub j_FiniMonTour(ByVal LeJoueur As Joueur) JoueurSuivant() End Sub ''' <summary> ''' Signale la fin de la partie ''' </summary> ''' <param name="LeJoueur"></param> ''' <param name="Kilometres"></param> Private Shared Sub j_JaiGagne(ByVal LeJoueur As Joueur, ByVal Kilometres As Integer) RaiseEvent PartieGagnee(LeJoueur) End Sub ''' <summary> ''' Démarre une nouvelle partie ''' </summary> Public Shared Sub NouvellePartie() 'génération des cartes Dim toutesLesCartes As List(Of Carte) = Kilometre.Generer() toutesLesCartes.AddRange(Embuche.Generer()) toutesLesCartes.AddRange(Parade.Generer()) toutesLesCartes.AddRange(Botte.Generer()) 'mélange des cartes Melanger(toutesLesCartes) 'distribution des cartes 6 par joueur, une par une chacun son tour For i As Integer = 0 To 5 For j As Integer = 0 To Moteur.Joueurs.Count - 1 Moteur.Joueurs(j).Pioche() Next j Next i indexJoueur = 0 AnnonceProchainJoueur() End Sub Private Shared indexJoueursDisqualifies As New List(Of Integer) ''' <summary> ''' Un joueur a dépassé la distance=> il est éliminé ''' </summary> ''' <param name="LeJoueur"></param> ''' <param name="Kilometres"></param> Private Shared Sub j_DistanceDepassee(ByVal LeJoueur As Joueur, ByVal Kilometres As Integer) indexJoueursDisqualifies.Add(Joueurs.IndexOf(LeJoueur)) 'on ajoute ce joueur dans la liste des joueurs disqualifiés End Sub ''' <summary> ''' Change le joueur après un coup fourré ''' </summary> ''' <param name="LeJoueur"></param> Private Shared Sub j_CoupFourre(ByVal LeJoueur As Joueur) indexJoueur = Joueurs.IndexOf(LeJoueur) AnnonceProchainJoueur() End Sub ''' <summary> ''' Annonce le joueur suivant ''' </summary> Private Shared Sub AnnonceProchainJoueur() RaiseEvent ProchainJoueur(JoueurEnCours) End Sub ''' <summary> ''' Surveille la pioche, et la refait à partir de la défausse quand elle est vide ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> Private Shared Sub Pioche_CollectionChanged(ByVal sender As Object, ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs) If e.Action <> System.Collections.Specialized.NotifyCollectionChangedAction.Remove Then Return End If If Pioche.Count = 0 Then Melanger(Defausse) RaiseEvent NouvellePioche() End If End Sub ''' <summary> ''' Mélange les cartes et (re)crée la pioche ''' </summary> ''' <param name="Cartes"></param> Private Shared Sub Melanger(ByVal Cartes As List(Of Carte)) Dim rnd As New Random(Date.Now.Millisecond) Do Dim index As Integer = rnd.Next(Cartes.Count) 'génère un entier aléatoire compris entre 0 et Cartes.Count -1 Pioche.Add(Cartes(index)) 'ajoute la carte à cet index dans la pioche Cartes.RemoveAt(index) 'enlève la carte de la collection d'origine Loop While Cartes.Count > 0 End Sub ''' <summary> ''' Passe au joueur suivant ''' </summary> Private Shared Sub JoueurSuivant() If indexJoueur = Joueurs.Count - 1 Then indexJoueur = 0 Else indexJoueur += 1 End If If indexJoueursDisqualifies.Contains(indexJoueur) Then 'si le joueur est disqualifié on passe au suivant de manière récursive JoueurSuivant() Else AnnonceProchainJoueur() End If End Sub End Class
On constate que les classes interagissent constamment. Cela implique d’avoir bien réfléchi au fonctionnement du logiciel avant de commencer à coder (d’où tout ce blabla avant de vous montrer un peu de code) et de coder un peu tout en même temps. Cette gymnastique fait partie de la difficulté à bien écrire une application tout objet.
D’ailleurs ça ne compile pas.
Evidement je ne vous ai pas fourni les modifications faites aux classes filles de Carte.
Voici Embuche, Parade et Botte
Public Class Embuche Inherits Carte ''' Code déjà écrit non retranscrit ''' <summary> ''' Méthode de classe qui génère la liste d'Embuches ''' </summary> ''' <returns></returns> Public Shared Function Generer() As List(Of Carte) Dim resultat As New List(Of Carte) For i As Integer = 0 To 4 resultat.Add(New Embuche("Image du feu rouge", TypeEmbuche.FeuRouge)) Next i For i As Integer = 0 To 3 resultat.Add(New Embuche("Image de la limitation de vitesse", TypeEmbuche.LimitationVitesse)) Next i For i As Integer = 0 To 2 resultat.Add(New Embuche("Image de la panne d'essence", TypeEmbuche.PanneEssence)) Next i For i As Integer = 0 To 2 resultat.Add(New Embuche("Image de la crevaison", TypeEmbuche.Crevaison)) Next i For i As Integer = 0 To 2 resultat.Add(New Embuche("Image de l'accident", TypeEmbuche.Accident)) Next i Return resultat End Function End Class Public Class Parade Inherits Carte ''' Code déjà écrit non retranscrit ''' <summary> ''' Méthode de classe qui génère la liste de Parades ''' </summary> ''' <returns></returns> Public Shared Function Generer() As List(Of Carte) Dim resultat As New List(Of Carte) For i As Integer = 0 To 13 resultat.Add(New Parade("Image du feu vert", TypeEmbuche.FeuRouge)) Next i For i As Integer = 0 To 5 resultat.Add(New Parade("Image de la fin de limitation", TypeEmbuche.LimitationVitesse)) Next i For i As Integer = 0 To 5 resultat.Add(New Parade("Image de la station essence", TypeEmbuche.PanneEssence)) Next i For i As Integer = 0 To 5 resultat.Add(New Parade("Image de la roue de secours", TypeEmbuche.Crevaison)) Next i For i As Integer = 0 To 5 resultat.Add(New Parade("Image de la réparation", TypeEmbuche.Accident)) Next i Return resultat End Function End Class Public Class Botte Inherits Carte ''' Code déjà écrit non retranscrit ''' <summary> ''' Cette méthode est appelée par le joueur en cas de coup fourré ''' </summary> Public Sub CoupFourreParLeJoueur() Me.Points += 300 End Sub ''' <summary> ''' Méthode de classe qui génère la liste de Bottes ''' </summary> ''' <returns></returns> Public Shared Function Generer() As List(Of Carte) Dim resultat As New List(Of Carte) resultat.Add(New Botte("Image du camion de pompier", TypeEmbuche.FeuRouge)) resultat.Add(New Botte("Image de la citerne d'essence", TypeEmbuche.PanneEssence)) resultat.Add(New Botte("Image de la roue increvable", TypeEmbuche.Crevaison)) resultat.Add(New Botte("Image de l'as du volant", TypeEmbuche.Accident)) Return resultat End Function End Class
Et pourquoi pas Kilometre ? C’est parce qu’avant, on va aborder un autre type de méthode
Un opérateur est une méthode particulière permettant de faire une opération entre deux objets, en général de même type.
Dans la classe Joueur, pour calculer la distance parcourue, j’ajoute la Distance de la dernière carte posée.
Je pourrais décider qu’un entier plus un Kilometre est possible, et que ça ajoute la Distance de ce Kilometre. Il me faudrait alors définir l’opérateur +.
On va commencer par l’opérateur + entre deux Kilometre, et ensuite par l’opérateur permettant d’additionner un entier et un Kilometre.
Public Class Kilometre Inherits Carte ''' Code déjà écrit non retranscrit ''' <summary> ''' Méthode de classe qui génère la liste de Kilomètres ''' </summary> ''' <returns></returns> Public Shared Function Generer() As List(Of Carte) Dim resultat As New List(Of Carte) For i As Integer = 0 To 9 resultat.Add(New Kilometre("Image 25 km", 25)) Next i For i As Integer = 0 To 9 resultat.Add(New Kilometre("Image 50 km", 50)) Next i For i As Integer = 0 To 9 resultat.Add(New Kilometre("Image 75 km", 75)) Next i For i As Integer = 0 To 11 resultat.Add(New Kilometre("Image 100 km", 100)) Next i For i As Integer = 0 To 3 resultat.Add(New Kilometre("Image 200 km", 200)) Next i Return resultat End Function ''' <summary> ''' Opérateur qui permet d'additionner deux Kilometre et de retourner un entier égal à la somme des distances. ''' </summary> ''' <param name="LUn"></param> ''' <param name="LAutre"></param> ''' <returns></returns> Public Shared Operator +(ByVal LUn As Kilometre, ByVal LAutre As Kilometre) As Integer Return LUn.Distance + LAutre.Distance End Operator ''' <summary> ''' Opérateur qui permet d'additionner un Kilometre et un entier ''' </summary> ''' <param name="Km"></param> ''' <param name="Entier"></param> ''' <returns></returns> Public Shared Operator +(ByVal Km As Kilometre, ByVal Entier As Integer) As Integer Return Km.Distance + Entier End Operator End Class
Cette implémentation ne respecte pas toutes les règles, il n’est pas vérifié que:
On pourrait parfaitement traiter ces règles rien qu’en regardant les cartes dans le jeu du joueur.
Cependant c’est l’occasion de traiter une option, bien pratique, des énumérations.
On peut faire en sorte que les valeurs d’énumération se cumulent.
Pour cela on ne va pas servir de la valeur à proprement dite, mais de sa représentation binaire.
Exemple avec 1 et 2 :
Si l’énumération vaut 01, alors c’est Valeur1 qui est sélectionné, si elle vaut 10, c’est Valeur2, et quand elle vaut 11 alors Valeur1 et Valeur2 sont cumulées.
Mais alors on ne peut pas définir Valeur3 = 3 ? Et bien non, il faut définir des valeurs dont la représentation binaire ne comporte qu’un seul 1 et ces valeurs doivent être toutes différentes.
Mais faut-il convertir toutes nos valeurs pour vérifier ces 2 conditions ? Non, il suffit de choisir la constante en puissance de 2 : 1, 2, 4, 8, 16, 32, 64, 256, 512, 1024, 2048, 4096 etc.
Cas particulier de la valeur 0, cette valeur existe, même si elle n’est pas définie, elle correspond à « Pas de Valeur » Si on ne définit pas 0, il peut arriver que cette valeur non prévue soit à l’origine de bugs. C’est pourquoi, il est fortement conseillé de la définir.
Dans notre cas, cette valeur sera utile donc la question ne se pose pas.
Choisir les valeurs adéquates ne suffit pas à rendre l’énumération cumulable, il faut ajouter l’attribut Flags à la déclaration de l’énumération.
<Flags()> Public Enum TypeEmbuche Aucune = 0 FeuRouge = 1 LimitationVitesse = 2 Crevaison = 4 PanneEssence = 8 Accident = 16 EndEnum
On cumule deux valeurs avec l’opérateur ou (l’opérateur
OR
) : FeuRouge ou LimitationVitesse vaut 01 ou 10 => 11, le cumul se fait bit à bit.
On enlève une valeur avec l’opérateur ou exclusif (l’opérateur
XOR
) : 11 ouex FeuRouge vaut 11 ouex 01 => 10 LimitationVitesse.
Attention, si on essaye d’enlever une valeur qui n’est pas présente, en fait ça l’ajoute (effet de bord de l’ouex: 01 ouex 10 => 11). Par contre, en ajoutant (avec un ou) une valeur déjà présente, cela ne change rien.
On peut savoir si l’énumération est égale à une valeur (cumulée ou non) par un test d’égalité classique, on peut aussi savoir si une valeur est présente dans l’accumulation avec la méthode HasFlag().
Ce point permet d’affiner la classe Botte, en effet, le camion de pompier immunise à la fois du feu rouge et de la limitation de vitesse.
''' <summary> ''' Réécriture de ToString pour afficher le nom de la botte ''' </summary> ''' <returns></returns> Public Overrides Function ToString() As String Select Case EmbucheImmunisee Case TypeEmbuche.Accident Return "As du volant" Case TypeEmbuche.Crevaison Return "Roue increvable" Case TypeEmbuche.FeuRouge Or TypeEmbuche.LimitationVitesse Return "Camion de pompier" Case TypeEmbuche.PanneEssence Return "Citerne d'essence" End Select 'on est obligé de mettre un default ou après le swicth return quelque chose car ToString retourne un résultat, et même si on a testé toutes les options de TypeEmbuche, le compilateur considère qu'un cas non prévu pourrait se présenter et qu'il n'y aurait alors pas de résultat Return "Erreur" End Function ''' <summary> ''' Méthode de classe qui génère la liste de Bottes ''' </summary> ''' <returns></returns> Public Shared Function Generer() As List(Of Carte) Dim resultat As New List(Of Carte) resultat.Add(New Botte("Image du camion de pompier", TypeEmbuche.FeuRouge Or TypeEmbuche.LimitationVitesse)) resultat.Add(New Botte("Image de la citerne d'essence", TypeEmbuche.PanneEssence)) resultat.Add(New Botte("Image de la roue increvable", TypeEmbuche.Crevaison)) resultat.Add(New Botte("Image de l'as du volant", TypeEmbuche.Accident)) Return resultat End Function
Ajoutons deux propriétés à Joueur du type TypeEmbuche, EmbucheEnCours qui va cumuler les embûches et Immunite qui va cumuler les immunités.
Au moment de joueur une carte, il conviendra de vérifier chaque règle.
A noter, pour commencer et après avoir paré un accident, une panne d’essence ou une crevaison, il faut un feu vert (sauf si on possède le camion de pompier). C’est exactement comme si on subissait un feu rouge. Donc on cumule l’embûche et le feu rouge.
Il faut donc modifier les méthodes Joue(Carte),Jouer (Embuche, Cible) et Jeu_CollectionChanged
Private Sub Jeu_CollectionChanged(ByVal sender As Object, ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs) If e.Action <> System.Collections.Specialized.NotifyCollectionChangedAction.Add Then Return End If If TypeOf Jeu.Last Is Embuche Then 'si la dernière carte reçue est une embuche, car Jeu contient des Cartes If Me.EmbucheRecue IsNot Nothing Then 'on vérifie que "quelqu'un" soit abonné à l'évènement Me.EmbucheRecue(Me) 'on émet l'évènement End If Dim lEmbuche As Embuche = CType(Jeu.Last, Embuche) 'pour travailler sur une Embuche, à partir d'une Carte, il faut "caster" (forcer la conversion) la Carte en Embuche 'on regarde si le joueur dispose de la botte pour faire un coup fourré For Each laCarte As Carte In Main If TypeOf laCarte Is Botte AndAlso (CType(laCarte, Botte)).EmbucheImmunisee = lEmbuche.Type Then Dim laBotte As Botte = CType(laCarte, Botte) 'on signale que l'on joue le coup fourré If Me.CoupFourre IsNot Nothing Then Me.CoupFourre(Me) End If laBotte.CoupFourreParLeJoueur() 'appelle la méthode qui va mettre le bonus de points Jouer(laBotte) 'On joue la botte Jeu.Remove(lEmbuche) 'on enlève l'embuche du jeu, ce qui va regénérer l'évènement Jeu_CollectionChanged, d'ou le premier test Moteur.Defausse.Add(lEmbuche) 'on jette l'embuche à la défausse Pioche() 'on pioche pour remplacer la botte Return 'on sort de la méthode, ça n'est pas la peine de continuer à cherche ce que l'on a déjà trouvé End If Next laCarte EmbucheEnCours = EmbucheEnCours Or lEmbuche.Type 'on ajoute l'embûche If lEmbuche.Type <> TypeEmbuche.LimitationVitesse AndAlso (Not Immunite.HasFlag(TypeEmbuche.FeuRouge)) Then EmbucheEnCours = EmbucheEnCours Or TypeEmbuche.FeuRouge 'on ajoute un feu rouge pour que le joueur soit obligé de mettre un feu vert End If ElseIf TypeOf Jeu.Last Is Kilometre Then 'si la dernière carte est un kilomètre, on calcule la distance totale. Dim km As Kilometre = CType(Jeu.Last, Kilometre) DistanceParcourue = km + DistanceParcourue If DistanceParcourue = Moteur.DistanceAParcourir Then If Me.JaiGagne IsNot Nothing Then Me.JaiGagne(Me,DistanceParcourue) 'on émet l'évènement avec le paramètre correspondant à la signature. End If ElseIf DistanceParcourue > Moteur.DistanceAParcourir Then If Me.DistanceDepassee IsNot Nothing Then Me.DistanceDepassee(Me,DistanceParcourue) End If End If End If End Sub Private Sub Joue(ByVal LaCarte As Carte) If TypeOf LaCarte Is Kilometre Then Select Case EmbucheEnCours Case TypeEmbuche.Aucune 'on peut rouler Case TypeEmbuche.LimitationVitesse 'il n'y a aucune autre embûche If (CType(LaCarte, Kilometre)).Distance > 50 Then RaiseEvent CarteInterdite(Me, LaCarte) Return 'on ne peut pas dépasser 50 End If Case Else 's'il y a n'importe quelle autre embuche, on ne peut pas rouler RaiseEvent CarteInterdite(Me, LaCarte) Return End Select If (CType(LaCarte, Kilometre)).Distance = 200 AndAlso Jeu.OfType(Of Kilometre).Count(Function(k) k.Distance = 200) = 2 Then 'On ne peut pas joueur plus de 2 "200 km" 'à noter pour cette recherche, au lieu d'imbriquer des boucles (comme plus haut), j'ai utilisé une requête Linq, qui filtre sur le type puis compte le nombre de "200 km" RaiseEvent CarteInterdite(Me, LaCarte) Return End If ElseIf TypeOf LaCarte Is Parade Then Dim maParade As Parade = CType(LaCarte, Parade) If Not EmbucheEnCours.HasFlag(maParade.EmbucheParee) Then RaiseEvent CarteInterdite(Me, LaCarte) Return 'on ne peut pas parer une embuche que l'on ne subit pas End If If maParade.EmbucheParee = TypeEmbuche.FeuRouge AndAlso Not(EmbucheEnCours = TypeEmbuche.FeuRouge OrElse EmbucheEnCours = (TypeEmbuche.FeuRouge Or TypeEmbuche.LimitationVitesse)) Then RaiseEvent CarteInterdite(Me, LaCarte) Return 'on ne peut poser un feu vert qui si on est en "état" de feu rouge, ou feu rouge et limitation, toutes les autres embuches doivent avoir été parées avant End If EmbucheEnCours = EmbucheEnCours Xor maParade.EmbucheParee Else 'c'est forcément une botte, elle peut se poser n'importe quand même sans parer une embûche Dim laBotte As Botte = (CType(LaCarte, Botte)) Immunite = Immunite Or laBotte.EmbucheImmunisee 'immunise If laBotte.EmbucheImmunisee.HasFlag(TypeEmbuche.FeuRouge) Then If EmbucheEnCours.HasFlag(TypeEmbuche.FeuRouge) Then EmbucheEnCours = EmbucheEnCours Xor TypeEmbuche.FeuRouge 'pare si besoin End If If EmbucheEnCours.HasFlag(TypeEmbuche.LimitationVitesse) Then EmbucheEnCours = EmbucheEnCours Xor TypeEmbuche.LimitationVitesse 'pare si besoin End If ElseIf EmbucheEnCours.HasFlag(laBotte.EmbucheImmunisee) Then EmbucheEnCours = EmbucheEnCours Xor laBotte.EmbucheImmunisee 'pare si besoin End If End If Main.Remove(LaCarte) 'On enlève la carte de la main Jeu.Add(LaCarte) 'on ajoute la carte au jeu AnnonceFinTour() End Sub Public Sub Jouer(ByVal Lembuche As Embuche, ByVal Cible As Joueur) If Cible Is Me Then 'si le joueur cible est le joueur en cours, on génère une erreur Throw New Exception("Le joueur ne peut pas se mettre une embuche à lui-même") End If Dim condition1 As Boolean = Cible.Immunite.HasFlag(Lembuche.Type) 'on ne peut pas poser ni une embûche contre laquelle la cible est immunisée, Dim condition2 As Boolean If Lembuche.Type = TypeEmbuche.LimitationVitesse Then condition2 = Cible.EmbucheEnCours.HasFlag(TypeEmbuche.LimitationVitesse) 'ni une limitation si elle y est déjà Else condition2 = Cible.EmbucheEnCours <> TypeEmbuche.Aucune AndAlso Cible.EmbucheEnCours <> TypeEmbuche.LimitationVitesse 'ni les autres embûches s'il y a en déjà une (sauf la limitation) End If If condition1 OrElse condition2 Then RaiseEvent CarteInterdite(Me, Lembuche) Else Main.Remove(Lembuche) 'On enlève la carte de la main Cible.Jeu.Add(Lembuche) 'on ajoute la carte au jeu du joueur cible AnnonceFinTour() End If End Sub
On doit aussi ajouter un évènement quand la carte n’est pas autorisée.
Public Delegate Sub EvenementCarte(LeJoueur As Joueur, LaCarte As Carte) 'signature de l'évènement, je déclare ce délégué en dehors de la classe, ainsi, il est visible dans tout l'espace de nom Public Class Joueur Public Event CarteInterdite As EvenementCarte 'évènement signalant que le joueur tente de poser une carte à l'encontre des règles.
Notez, que le délégué est déclaré en dehors de la classe, ainsi il est public pour tout l’espace de nom. Le moteur peut ainsi générer un évènement avec une signature identique :
''' <summary> ''' Transmet l'alerte qu'un joueur tente de joueur une carte "illégale" ''' </summary> ''' <param name="LeJoueur"></param> ''' <param name="LaCarte"></param> Private Shared Sub j_CarteInterdite(LeJoueur As Joueur, LaCarte As Carte) RaiseEvent CarteInterdite(LeJoueur, LaCarte) End Sub
Et puis tant qu’on y est, on va faire en sorte que le moteur fasse piocher le joueur quand c’est son tour.
''' <summary> ''' Annonce le joueur suivant ''' </summary> Private Shared Sub AnnonceProchainJoueur() JoueurEnCours.Pioche() 'fait piocher le joueur suivant RaiseEvent ProchainJoueur(JoueurEnCours)
Admettons, que nous voulions choisir le niveau d’un joueur, on créerait une énumération qui reflète la difficulté du jeu
Public Enum Difficulte Facile Normale Difficile Expert End Enum
Et partant du principe que le niveau d’un joueur doit être initialisé dès l’instanciation de celui-ci, on va mettre ce paramètre dans la signature du constructeur.
Pour ne pas changer tout notre projet, on crée une surcharge avec laquelle on définit ce niveau.
Dans le constructeur initial, on met le niveau à une valeur par défaut.
Public Sub New(ByVal LeNom As String) Nom = LeNom Main = New List(Of Carte) Jeu = New ObservableCollection(Of Carte) 'initialisation de la collection AddHandler Jeu.CollectionChanged, AddressOf Jeu_CollectionChanged 'Abonnement à l'évènement signalant un changement Niveau = Difficulte.Facile NouvellePartie() End Sub Public Sub New(ByVal LeNom As String, ByVal LeNiveau As Difficulte) Nom = LeNom Main = New List(Of Carte) Jeu = New ObservableCollection(Of Carte) 'initialisation de la collection AddHandler Jeu.CollectionChanged, AddressOf Jeu_CollectionChanged 'Abonnement à l'évènement signalant un changement Niveau = LeNiveau NouvellePartie() End Sub
Mais là, les 2 méthodes sont quasiment identiques, on devrait pouvoir factoriser ?
Et bien, il est possible qu’un constructeur en appelle un autre.
Le constructeur qui n’a que le nom en paramètre, peut appeler le 2eme et lui passer le niveau par défaut.
Public Sub New(ByVal LeNom As String) Me.New(LeNom,Difficulte.Facile) 'on appelle le constructeur plus complet avec une valeur par défaut End Sub Public Sub New(ByVal LeNom As String, ByVal LeNiveau As Difficulte) Nom = LeNom Main = New List(Of Carte) Jeu = New ObservableCollection(Of Carte)) 'initialisation de la collection AddHandler Jeu.CollectionChanged, AddressOf Jeu_CollectionChanged 'Abonnement à l'évènement signalant un changement Niveau = LeNiveau NouvellePartie() End Sub
Plus haut, je vous ai expliqué qu’en .Net, il n’est pas possible de dériver de plusieurs classes mères. Cela pourrait pourtant parfois s’avérer pratique.
Par exemple, si on a une List d’entiers (12, 31, 5, 19, 3, 26, 2, 16, 8) que l’on veut trier ; il y a la méthode Sort().
Mais si dans la classe Joueur, j’essaie de trier la Main (Main.Sort()), ça ne fonctionne pas, ça génère même une exception.
Celle-ci est due au fait que pour être triable la classe doit « dériver » de quelque chose qui permet de comparer deux Cartes.
Cependant, ça n’est pas possible, mais en plus, on ne peut pas comparer des Kilometres de la même façon que des cafetières.
Il nous faut donc un mécanisme qui permette au développeur de savoir ce qu’il doit faire, une sorte d’héritage qui décrive quelle méthode doit être écrite.
Ce sont les interfaces.
Pour Sort, il faut implémenter IComparable
Public MustInherit Class Carte Implements IComparable(Of Carte)
Et ça ne compile pas ?
C’est parce qu’en implémentant une interface, on n’hérite pas d’une (ou plusieurs) méthode(s), mais de l’obligation de l’(es) écrire.
Pour VB.Net, il faut se servir du message d’erreur, on voit que c’est une fonction retournant un entier et qui prend une Carte en paramètre.
Public Function Compare(Other As Carte) As Integer End Function
Cependant, on ne sait pas trop quoi en faire, pas le choix un petit tour sur MSDN (d'abord) ou dans les forums, pour découvrir que Compare retourne
Reste à définir comment on compare des Kilometre, des Embuches, des Parades et des Bottes.
Je choisis qu’en premier viennent les bottes par ordre alphabétique, puis les Feux Verts, puis les Embuches, par ordre alphabétique, intercalés avec la Parade associée et enfin les Kilometre par ordre de distance.
On pourrait écrire une méthode compliquée qui fait ce tri, mais il est plus simple de définir, dans Carte, une propriété abstraite, en lecture seule qui retourne une valeur facilement comparable (un entier), qui respecte l’ordre défini.
Notre méthode Compare, comparera cette propriété entre les 2 instances.
''' <summary> ''' Propriété pour le tri ''' </summary> Public MustOverride ReadOnly Property Ordre As Integer ''' <summary> ''' Implémentation de IComparable<Carte> ''' </summary> ''' <param name="other"></param> ''' <returns></returns> Public Function CompareTo(ByVal other As Carte) As Integer Return Me.Ordre.CompareTo(other.Ordre) End Function Ensuite, on implémente la propriété dans chaque classe fille, en commençant par les Bottes ''' <summary> ''' Implémentation de ordre dans Botte ''' </summary> Public Overrides ReadOnly Property Ordre As Integer Get Select Case EmbuheImmunisee Case TypeEmbuche.Accident 'As du Volant Return 0 Case TypeEmbuche.FeuRouge Or TypeEmbuche.LimitationVitesse 'Camion de Pompier Return 1 Case TypeEmbuche.PanneEssence 'Citerne d'essence Return 2 Case Else 'il ne reste que la roue de secours Return 3 End Select End Get End Property
Ensuite les Embuche et les Parades en intercalage, avec le Feu Vert devant (il prendra la valeur 4, à la suite des Bottes).
Pour se simplifier la tache nous allons changer les valeurs de TypeEmbuche, de façon à se quelle correspondent à l’ordre alphabétique, qu’il soit possible d’ajouter 1 pour intercaler la parade sans conflit tout en respectant les « valeurs flagables ».
Aucune = 0 FeuRouge = 8 LimitationVitesse = 16 Crevaison = 4 PanneEssence = 32 Accident = 2
L’accident devra donc prendre la valeur 5, la réparation 6 et ainsi de suite.
Pour les Embuches
''' <summary> ''' Implémentation de ordre dans Embuche ''' </summary> Public Overrides ReadOnly Property Ordre As Integer Get Return CInt(Type) + 3 End Get End Property
Pour les Parades
''' <summary> ''' Implémentation de ordre dans Parade ''' </summary> Public Overrides ReadOnly Property Ordre As Integer Get If EmbucheParee = TypeEmbuche.FeuRouge Then Return 4 Else Return CInt(EmbucheParee) + 4 End If End Get End Property
Enfin, le plus petit Kilometre devra avoir une valeur supérieure à 37.
''' <summary> ''' Implémentation de ordre Kilometre ''' </summary> Public Overrides ReadOnly Property Ordre As Integer Get Return Distance + 20 End Get End Property
Malheureusement, ObservableCollection ne dispose pas de la méthode Sort(), cependant grâce à la propriété Ordre, on peut savoir « où » déposer une carte dans le jeu.
Dim superieure As Carte = Jeu.FirstOrDefault(Function(c) c.Ordre > LaCarte.Ordre) 'on cherche la première carte dont l'ordre est supérieur à la carte jouée, si elle existe. If superieure IsNot Nothing Then Jeu.Insert(Jeu.IndexOf(superieure), LaCarte) 'si la carte supérieure existe, on insère la carte jouée à l'index de la carte supérieure Else Jeu.Add(LaCarte) 'sinon on ajoute la carte jouée au jeu (donc à la fin) End If
Cela implique une petite modification de la méthode Jeu_CollectionChanged, en effet, nous testions la dernière carte de la collection, il faut maintenant chercher la carte ajoutée là ou elle est grâce à son index fournit par
e.NewStartingIndex
Une interface nous oblige à écrire une ou plusieurs méthodes, il n’y a donc pas de factorisation de code, par contre on peut profiter de tous les avantages du polymorphisme. Et contrairement à l’héritage, on peut implémenter plusieurs interfaces tout en héritant d’une classe.