Activité pratique 2

Durée2h30

Consignes globales

Cette séance complète la séance précédente en utilisant les mêmes techniques, cette fois pour concevoir une application complète, en l’occurrence un jeu de société de bonne facture, Citadelles. En effet, ces techniques passent à l’échelle et permettent de concevoir des applications complexes.

Important

De même que pour la séance précédente vous n’allez pas écrire du code, vous allez réfléchir à comment structurer une solution à un problème donné et vous donnerez le pseudo-code correspondant. Vous n’avez donc pas besoin d’utiliser votre ordinateur (du moins dans un premier temps), un simple papier/crayon suffit.

Contenu de l’activité

Le jeu Citadelles, créé en 2000, par Bruno Faidutti combine hasard, réflexion et surprise (chaque joueur/joueuse n’a qu’une vision partielle de la situation).

Il s’agit d’arriver le plus loin dans la construction d’une citadelle aboutie avec ses “quartiers” relevant de la religion, de la noblesse, du commerce, de la soldatesque et du merveilleux. Pour cela chaque joueur.euse dispose d’un trésor et va incarner, à chaque tour, un rôle (assassin, voleur, magicien, roi, évêque, marchand, architecte, condottiere), avec ses pouvoirs spécifiques. Ce rôle est masqué (du moins initialement) à ses adversaires.

Il existe trois éditions du jeu. Nous allons nous baser sur la seconde édition en considérant :

  • les règles pour jouer à deux et
  • en nous limitant aux personnages introduits dans la première édition.

Les règles du jeu sont disponibles sur Internet (voir jeuxavolonte.asso.fr). Les annexes vous donnent différentes indications disponibles sur les cartes du jeu, qui ne sont pas toujours disponibles sur Internet et varient d’une édition à l’autre.

Nous allons nous intéresser à une une version informatisée de ce jeu permettant à deux joueur.euse.s humains de se rencontrer ou, plus précisément, à une seule personne de reproduire une partie à deux joueur.euse.s (l’application propose pour chaque les choix légaux possibles).

Une partie importante de la conception consiste à préciser les contours de l’application à partir d’une description informelle de ce que l’on souhaite réaliser, qui prend ici la forme des règles du jeu. Comme on se focalise ici sur un jeu à deux et un nombre limité de personnages, vous pouvez survoler les règles qui régissent le choix des personnage, hors jeu à deux, ainsi que la description des “nouveaux” personnages.

Les méthodologies de conception associées à la programmation orientée-objet (on dit aussi programmation par objets, cf. FranceTerme) ont la bonne propriété de conduire naturellement à une approche incrémentale qui découpe le problème en sous-problèmes et raffine les solutions associés aux sous-problèmes.

En pratique, au niveau de granularité de notre application, le découpage va correspondre à un découpage en classes (pour des applications de plus grande taille, on peut être amené(e) à considérer d’autres niveaux de granularité qui prendront le nom de composants, modules, couche, systèmes…). On va donc dans un premier temps essayer de faire émerger les classes qui pourraient avoir du sens par rapport à l’application (voir le cours sur l’identification des classes).

On peut ensuite essayer de remplir un tableau Classes, objets, attributs… Ce remplissage va aussi se faire de manière incrémentale. La définition des responsabilités associées à chaque classe et des relations/associations entre classes notamment peut remettre en question le découpage initial, c’est-à-dire faire apparaître ou disparaître des classes, ou encore conduire à modifier le nom des classes afin de mieux capturer leur rôle dans l’application.

Ici, on va commencer par s’intéresser aux classes qui permettent de gérer les cartes. Intuitivement, ces classes constituent une forme de socle (ou couche basse) sur lequel les autres classes vont s’appuyer, c’est-à-dire que ces autres classes vont utiliser les classes du socle mais l’inverse ne va pas être vrai. C’est bien sûr une forme d’architecture logicielle qui n’apparait pas systématiquement mais qui permet un découpage utile : on construit des fondations stables sur lesquelles les couches supérieures vont pouvoir s’appuyer.

Une fois le tableau Classes, objets, atributs… disponible, il est possible de préciser le contenu des méthodes.

Quel que soit l’étape de conception, il est difficile d’apprécier la solidité de l’édifice, même si en avançant dans le processus on a plus d’éléments pour le faire. Si des éléments de conception s’avèrent ne pas tenir la route, il ne faut pas hésiter à revenir en arrière (sans détruire ce qui a déjà été fait parce qu’il arrive aussi qu’on remette en question des points qui finalement tenaient la route).

Il y a aussi des choix à faire. Certains peuvent être meilleurs que d’autres, dans l’absolu (l’implémentation de la conception sera problématique sinon) ou du fait de connaissances additionnelles sur l’application (environnement technique, propriétés dites “non fonctionnelles” de performance, sécurité, maintenabilité, extensibilité…"). Il est en général impossible de tous les explorer. À défaut d’expérience, la simplicité peut initialement être un bon critère de choix.

Les classes candidates

À la simple lecture de la règle du jeu, quelles sont les classes qui vous semblent utiles ? Essayez de distinguer celles qui le sont sûrement et celles qui le sont peut-être.

À faire

Listez les classes qui vous semblent utiles, en séparant celles qui le sont sûrement de celles qui le sont peut-être.

Correction
Cliquez ici pour afficher la correction.

Cette correction décrit une solution possible qui peut diverger de celle co-construite en classe. Le cadre est une partie à 2 players, avec les 8 personnages.

Notations : [T] désigne une séquence ordonnée et {T} un ensemble sans ordre ni doublon. Aucun signifie que la méthode ne retourne rien. Les attributs sont privés : on ne modifie un objet qu’avec ses méthodes (sauf raccourcis signalés dans le pseudo-code).

Sûrement : Quartier, Personnage, Player, Partie, et les regroupements de cartes Pioche, Main, Cité.

Peut-être : Carte (une abstraction au-dessus de Quartier et Personnage), Personnages (les personnages disponibles et l’écart), Mes_personnages (les 0 à 2 personnages d’un player), IHM (les interactions), Plateau, Couronne, Trésor.

Trois candidates sont écartées en avançant :

  • Plateau ne ferait que regrouper des éléments partagés. Partie joue ce rôle de meneur de jeu : elle contient ces éléments et sollicite les players à tour de rôle.
  • Couronne n’a ni attribut ni comportement propre : le booléen est_couronné de Player suffit.
  • Trésor se réduit à un entier chez chaque player et à un autre chez Partie (la banque, 30 pièces).

On écrit player plutôt que joueur.euse pour rester inclusif sans alourdir le texte.

Les cartes

On va tout d’abord s’intéresser aux cartes du jeu, individuellement, puis dans leur utilisation.

Les cartes, inviduellement

Types et hiérarchie des cartes

Listez, en leur donnant un nom, les différents types de cartes qui apparaissent dans le jeu. Organisez-les en une hiérarchie.

À faire

Listez les types de cartes du jeu et organisez-les en une hiérarchie.

Correction
Cliquez ici pour afficher la correction.
Carte (abstraite)
├── Quartier      nom, coût, catégorie ∈ {Religion, Noblesse, Commerce, Soldatesque, Merveille}
└── Personnage    rôle ∈ {Assassin, Voleur, Magicien, Roi, Évêque, Marchand, Architecte, Condottiere}

Carte est abstraite : aucun objet n’en est créé, elle sert seulement à regrouper ce que Quartier et Personnage ont en commun (être une carte, donc pouvoir être piochée, tenue en main, posée).

La couronne n’a pas de classe : on la représente par le booléen est_couronné de Player.

Les attributs d’une carte

Déterminez les attributs nécessaires à la gestion d’une carte.

À faire

Donnez les attributs de chaque type de carte.

Correction
Cliquez ici pour afficher la correction.
Classe Attributs
Carte (abstraite) (aucun)
Quartier nom: [Caractère]
coût: Entier
catégorie: [Caractère]
Personnage rôle: [Caractère]

Deux choix méritent d’être justifiés.

  • Des attributs plutôt qu’une sous-classe par quartier. Plusieurs cartes identiques existent (5 tavernes, par exemple) et elles ne diffèrent que par leurs valeurs, pas par leur comportement : des attributs suffisent. nom sert à l’affichage et à la règle qui interdit deux quartiers identiques dans une même cité.
  • Un simple attribut rôle sur Personnage. C’est le choix le plus simple : rôle ne prend que 8 valeurs et il n’existe qu’une carte par rôle. Son inconvénient est de centraliser toutes les spécificités des rôles dans une seule méthode, Player.agit(Personnage) ; la dernière question de cette activité revient dessus.

Les cartes, dans leur utilisation

On s’intéresse à la fois au type des cartes regroupées et à la manière dont les cartes sont utilisées. Par exemple, si on considère une pioche, une fois la pioche créée, on peut la mélanger, piocher une carte et se défausser d’une carte (la question peut alors être de savoir où mettre la carte, sur ou sous la pioche ?). Quand on a plusieurs types de cartes, la pioche contient des cartes d’un type donné.

Une autre propriété qu’il est utile de repérer est si le regroupement est utilisé par un joueur.euse unique ou partagé entre plusieurs. Par exemple, une pioche est typiquement partagée alors qu’une main est sous le contrôle d’un joueur.euse, qu’elle soit visible ou pas des autres.

Les types de regroupements

À partir de la règle du jeu, déterminez les différents types de regroupements de cartes utilisés au cours de la partie en leur donnant un nom, en y associant un type de cartes et en indiquant s’ils sont partagés ou pas.

À faire

Pour chaque regroupement de cartes : un nom, le type de cartes qu’il contient, et s’il est partagé ou non.

Correction
Cliquez ici pour afficher la correction.
Regroupement Contient Partagé ?
Pioche une séquence de Quartier (l’ordre compte : on pioche sur le dessus, on défausse en dessous) partagée
Personnages les 8 Personnage, répartis entre ceux qui restent disponibles et l’écart partagé
Main un ensemble de Quartier propre à un player, caché
Cité un ensemble de Quartier propre à un player, visible
Mes_personnages un ensemble de Personnage (2 au plus, à deux joueurs) propre à un player, caché

Seule la pioche est ordonnée, d’où une séquence [Quartier] ; les autres sont des ensembles {...}, sans ordre ni doublon.

Le tableau des classes de regroupements

Il s’agit maintenant d’être un peu plus spécifique en considérant chacun des types de la question précédente comme une classe et de remplir le tableau Classes, objets, attributs… correspondant.

Vous avez précédemment utilisé des listes. Elles sont utiles ici pour représenter des séquences de cartes, par exemple le type d’une séquence de personnages sera [Personnage]. De même, on peut convenir de représenter le type d’un ensemble de personnages par {Personnage}.

À faire

Remplissez le tableau Classes, objets, attributs, méthodes pour ces cinq regroupements.

Correction
Cliquez ici pour afficher la correction.
Nom de classe Attributs Méthodes Objets créés de la classe Objets d’autres classes associés
Pioche cartes: [Quartier] pioche() -> Quartier
défausse(quartier: Quartier) -> Aucun (remet la carte sous la pioche)
mélange() -> Aucun
pioche : 1 objet partagé, créé par Partie. Les 57 Quartier qu’elle contient
Personnages ordre: [Personnage] (les 8, dans l’ordre d’appel)
disponibles: {Personnage}
écart: {Personnage}
écarte_personnage() -> Aucun (au hasard, de disponibles vers écart)
choisit_personnage() -> Personnage (choix via IHM, retiré de disponibles)
dans_l_ordre() -> [Personnage]
réinitialise() -> Aucun
personnages : 1 objet partagé, créé par Partie. Les 8 Personnage
IHM (utilisée pour le choix)
Main quartiers: {Quartier} ajoute(quartier: Quartier) -> Aucun
efface(quartier: Quartier) -> Aucun
est_vide() -> Booléen
taille() -> Entier
élément(n: Entier) -> Quartier
vide() -> {Quartier} (vide la main et renvoie son contenu, pour l’échange du Magicien)
main de player_A et main de player_B : 2 objets, chacun créé par le constructeur de Player. Les Quartier en main
Cité quartiers: {Quartier} ajoute(quartier: Quartier) -> Booléen (vrai si la cité atteint 8 quartiers)
retire(quartier: Quartier) -> Aucun (Condottiere)
contient(quartier: Quartier) -> Booléen (comparaison par nom)
compte(catégorie: [Caractère]) -> Entier
est_complète() -> Booléen
points() -> Entier
cité de player_A et cité de player_B : 2 objets, chacun créé par le constructeur de Player. Les Quartier construits
Mes_personnages personnages: {Personnage} (2 au plus) ajoute(personnage: Personnage) -> Aucun
contient(personnage: Personnage) -> Booléen
réinitialise() -> {Personnage}
mes_personnages de player_A et de player_B : 2 objets, chacun créé par le constructeur de Player. 0 à 2 Personnage

C’est Cité.ajoute — et non Main.ajoute — qui renvoie le booléen de fin de partie : c’est bien la cité qui atteint 8 quartiers.

Mise en place d’une partie simple

Dans un premier temps, toujours dans l’optique de travailler incrémentalement, on va se focaliser sur le déroulement d’un tour de jeu, sans prendre en compte les pouvoirs des personnages.

Les autres classes et le lancement de la partie

Maintenant que vous avez une meilleure compréhension des cartes utilisées, commencez à compléter le tableau Classes, objets, attributs… pour les autres classes identifiées initialement, en vous focalisant sur leurs attributs. Pour ce qui est des méthodes, quelle méthode va lancer la partie, et, à très gros grain, comment vont se passer les interactions entre les principaux objets impliqués ?

La gestion de la partie est complexe, d’après la règle, elle peut être naturellement découpée en trois phases : le choix des personnages, les tours des joueur.euse.s et le décomptage des points. On pourrait créer une classe Phase puis créer des sous-classes pour chacune des sous-phases, spécialisée avec le comportement spécifique de la sous-phase. Mais on peut aussi découper la méthode de jeu en trois sous-méthodes qui vont prendre en charge chacune des phases.

Mais, au fait, n’y a-t-il pas quelque chose à faire avant de lancer le choix des personnages ?

À faire

Complétez le tableau pour Player, Partie et IHM. Quelle méthode lance la partie, et que faut-il faire avant le choix des personnages ?

Correction
Cliquez ici pour afficher la correction.
Nom de classe Attributs Méthodes Objets créés de la classe Objets d’autres classes associés
Player nom: [Caractère]
trésor: Entier
est_couronné: Booléen
main: Main
cité: Cité
mes_personnages: Mes_personnages
partie: Partie
assassinat: [Personnage] (0 ou 1 élément)
vol: [Personnage] (0 ou 1 élément)
Publiques : choisit_premier_personnage()
choisit_personnage()
joue(personnage: Personnage) -> Booléen
enregistre_assassinat(p: Personnage)
enregistre_vol(p: Personnage)
réinitialise_tour()
score() -> Entier
Auxiliaires : choisit()
construit(n: Entier) -> Booléen
agit(personnage: Personnage)
collecte(catégorie: [Caractère])
défausse(n: Entier)
player_A et player_B : 2 objets, créés par le constructeur de Partie. main, cité, mes_personnages : composition
partie : référence vers le meneur de jeu
assassinat, vol
IHM
Partie player_A: Player
player_B: Player
personnages: Personnages
pioche: Pioche
trésor: Entier (la banque, 30 pièces)
Publique : joue() -> Aucun
Auxiliaires : choisit_personnages()
joue_tour() -> Booléen
réinitialise_tour()
compte_points()
autre(player: Player) -> Player
partie : 1 objet, point d’entrée de l’application. player_A, player_B, personnages, pioche
IHM (singleton) (aucun) choisit(message: [Caractère], choix: [T]) -> T
affiche(x) -> Aucun
IHM : instance unique, utilisée sans être instanciée explicitement. Aucun (classe technique, utilisée par Player et Personnages)

Player connaît sa partie parce qu’un player doit accéder aux éléments partagés : la pioche, la banque, les personnages et l’autre player.

La méthode qui lance la partie est Partie.joue(). Et oui, il y a bien quelque chose à faire avant le choix des personnages : la mise en place, que l’on confie au constructeur.

CLASS Partie:
  CONSTRUCTOR Partie():
    self.trésor = 30
    self.pioche = Pioche()                 # crée les 57 quartiers
    self.pioche.mélange()
    self.personnages = Personnages()       # crée les 8 personnages
    self.player_A = Player("A", self)      # le constructeur de Player crée main, cité, mes_personnages
    self.player_B = Player("B", self)
    self.player_A.est_couronné = True
    for player in [self.player_A, self.player_B]:
      self.trésor = self.trésor - 2 ; player.trésor = 2
      for i in 1 à 4: player.main.ajoute(self.pioche.pioche())

Le choix des personnages

Le déroulement du choix

Analyser la manière dont se passe le choix des joueur.euse.s dans le cas d’une partie à 2 en déterminant notamment les actions élémentaires qui reviennent et la manière dont elles s’enchaînent, ce qui pourra donner lieu à une première définition textuelle de la phase, qu’il sera ensuite aisée de transformer en pseudo-code. Cette analyse devrait aussi contribuer à enrichir le tableau Classe, objets, attributs…

À faire

Décrivez le déroulement du choix des personnages à deux, puis donnez-en le pseudo-code.

Correction
Cliquez ici pour afficher la correction.

À deux, le tirage est irrégulier : le player couronné écarte un personnage puis choisit le sien, alors que les choix suivants sont suivis d’un écart. Chacun finit avec deux personnages.

Deux méthodes de Player rendent cette irrégularité visible plutôt que de la cacher dans un compteur :

CLASS Player:
  METHOD choisit_premier_personnage() -> Aucun:
    self.partie.personnages.écarte_personnage()
    self.mes_personnages.ajoute(self.partie.personnages.choisit_personnage())

  METHOD choisit_personnage() -> Aucun:
    self.mes_personnages.ajoute(self.partie.personnages.choisit_personnage())
    self.partie.personnages.écarte_personnage()

La phase complète, côté Partie :

CLASS Partie:
  METHOD choisit_personnages() -> Aucun:
    if self.player_A.est_couronné: p1, p2 = self.player_A, self.player_B
    else:                          p1, p2 = self.player_B, self.player_A
    p1.choisit_premier_personnage()    # écarte puis choisit
    p2.choisit_personnage()            # choisit puis écarte
    p1.choisit_personnage()
    p2.choisit_personnage()

Les tours des joueurs

Le pseudo-code d’un tour

Commençons par nous intéresser à l’organisation d’un tour. Il faut d’abord faire jouer les joueur.euse.s dans l’ordre prédéfini des personnages. Voici une suggestion pour ce faire. Si on dispose de la séquence des personnages, par exemple dans l’attribut personnages d’une partie (une donnée utile pour sa réinitialisation entre chaque tour), il est possible de traverser cette séquence et demander à chaque joueur.euse.s de jouer son tour s’il a le personnage (s’il ne l’a pas, rien ne va se passer).

Proposez un pseudo-code pour un tour de jeu, sans oublier de tenir compte de la possibilité que ce tour de jeu termine la partie et complétez le tableau des classes.

  • Gestion des choix : on suppose qu’on dispose d’une méthode qu’il est possible de directement appliquer à la classe IHM sans fabriquer d’instance choisit(message: [Character], [T]) -> T où message est le message à afficher, [T] la séquence, aussi à afficher, des choix possibles, avec T le type des choix. La méthode retourne le choix sélectionné. Concrètement, cette méthode affiche la séquence des choix en les numérotant et requiert l’entrée d’un nombre correspondant à son choix (qui doit être dans le bon intervalle sinon la méthode boucle) et retourne le choix correspondant.

    Par exemple, pour demander un choix de personnages dans une séquence de personnages personnages et récupérer le résultat, il suffit d’écrire personnage = IHM.choisit("Choisissez un personnage", personnages).

  • Opérations sur les séquences : on suppose qu’on peut accèder à l’élément de rang n d’une séquence de type [T] via la méthode élément(n: Entier) -> T. Plutôt qu’écrire seq.élément(n), on pourra écrire seq[n].

À faire

Proposez le pseudo-code d’un tour de jeu, sans oublier qu’un tour peut terminer la partie.

Correction
Cliquez ici pour afficher la correction.

Le tour parcourt les personnages dans l’ordre d’appel et demande à chaque player de jouer, ce qui ne produit rien s’il n’a pas le personnage appelé :

CLASS Partie:
  METHOD joue_tour() -> Booléen:
    self.choisit_personnages()
    terminé = False
    for personnage in self.personnages.dans_l_ordre():
      fin_A = self.player_A.joue(personnage)
      fin_B = self.player_B.joue(personnage)
      terminé = terminé or fin_A or fin_B   # on va au bout du tour en cours
    return terminé

Côté player, un tour se résume à : agir selon son rôle, prendre des ressources, puis construire.

CLASS Player:
  METHOD joue(personnage: Personnage) -> Booléen:
    if not self.mes_personnages.contient(personnage) or self.assassinat == [personnage]:
      return False
    if self.vol == [personnage]:                    # le voleur prend tout l'or
      voleur = self.partie.autre(self)
      voleur.trésor = voleur.trésor + self.trésor
      self.trésor = 0
    self.agit(personnage)                           # pouvoir du rôle (en début de tour)
    self.choisit()
    if personnage.rôle == "Architecte": return self.construit(3)
    else:                               return self.construit(1)

  METHOD choisit() -> Aucun:
    action = IHM.choisit("Choisissez une action",
                         ["Prendre 2 pièces d'or", "Piocher 2 cartes et en garder une"])
    if action == "Prendre 2 pièces d'or":
      self.partie.trésor = self.partie.trésor - 2
      self.trésor = self.trésor + 2
    else:
      cartes = [self.partie.pioche.pioche(), self.partie.pioche.pioche()]
      gardée = IHM.choisit("Choisissez une carte à garder", cartes)
      self.main.ajoute(gardée)
      if cartes[1] == gardée: self.partie.pioche.défausse(cartes[2])
      else:                   self.partie.pioche.défausse(cartes[1])

  METHOD construit(n: Entier) -> Booléen:
    if n == 0 or self.main.est_vide(): return False
    constructibles = []
    for rang in 1 à self.main.taille():
      q = self.main[rang]
      if self.trésor >= q.coût and not self.cité.contient(q):
        constructibles.ajoute(q)
    if constructibles == []: return False
    if IHM.choisit("Construire un quartier ?", ["Oui", "Non"]) == "Non": return False
    q = IHM.choisit("Choisissez un quartier à construire", constructibles)
    self.main.efface(q)
    self.trésor = self.trésor - q.coût
    self.partie.trésor = self.partie.trésor + q.coût
    if self.cité.ajoute(q): return True             # 8 quartiers : fin de partie
    return self.construit(n - 1)

construit est paramétrée par le nombre de constructions : l’Architecte en fait jusqu’à 3, les autres une seule. Le paramètre évite de dupliquer la méthode.

L’enchaînement des tours et la fin de partie

Il s’agit maintenant d’enchaîner les tours et de clore la partie, concrètement de finaliser le pseudo-code qui gère une partie.

À faire

Finalisez le pseudo-code de la partie : enchaînement des tours, réinitialisation, décompte.

Correction
Cliquez ici pour afficher la correction.
CLASS Partie:
  METHOD joue() -> Aucun:
    terminé = False
    while not terminé:
      terminé = self.joue_tour()
      self.réinitialise_tour()
    self.compte_points()

  METHOD réinitialise_tour() -> Aucun:
    # la couronne revient à qui détient le Roi, même assassiné
    for player in [self.player_A, self.player_B]:
      if player.mes_personnages.contient(roi):
        self.player_A.est_couronné = (player == self.player_A)
        self.player_B.est_couronné = (player == self.player_B)
    self.personnages.réinitialise()
    self.player_A.réinitialise_tour()      # vide mes_personnages, assassinat, vol
    self.player_B.réinitialise_tour()

Deux points à noter :

  • le tour en cours se termine même si un player a fini sa cité : joue_tour accumule les fins dans terminé au lieu de sortir de la boucle ;
  • joue() reste courte parce que chaque phase est confiée à une méthode auxiliaire (choisit_personnages, joue_tour, réinitialise_tour, compte_points).

Prise en compte des rôles

Premier survol de la question

Pour commencer, on va (à nouveau) simplifier le problème en considérant que l’action associée au rôle du personnage a lieu à un moment déterminé du tour, par exemple au début. Une première idée consiste donc à définir dans la classe Joueur une méthode auxiliaire agit(Personnage) -> Aucun qui va dérouler les actions associées au rôle.

La structure de la méthode est la suivante :

METHOD agit(personnage: Personnage) -> Aucun:
  rôle = personnage.rôle
  if rôle == "Assassin":
    ...
  else if rôle == "Voleur"
    ...
  else ...

Ce qu’on peut aussi noter, en utilisant une construction dite de pattern matching :

METHOD agit(personnage: Personnage) -> Aucun:
  personnage.rôle match
    case "Assassin":
      ...
    case "Voleur"
      ...
    case ...
Revue des rôles

Passez en revue les différents rôles et essayez de déterminer s’il y a des ajouts à faire à la conception initiale (attributs, méthodes auxiliaires).

À faire

Passez en revue les huit rôles et notez ce qu’ils ajoutent à la conception (attributs, méthodes auxiliaires).

Correction
Cliquez ici pour afficher la correction.
CLASS Player:
  METHOD agit(personnage: Personnage) -> Aucun:
    autre = self.partie.autre(self)
    personnage.rôle match
      case "Assassin":    cible = IHM.choisit("Qui assassiner ?", <rôles 2 à 8 hors les miens>)
                          if autre.mes_personnages.contient(cible): autre.enregistre_assassinat(cible)
      case "Voleur":      cible = IHM.choisit("Qui voler ?", <rôles 3 à 8 hors les miens et hors assassiné>)
                          if autre.mes_personnages.contient(cible): autre.enregistre_vol(cible)
      case "Magicien":    échange de main avec autre (via Main.vide)
                          OU défausse(n) puis pioche n cartes
      case "Roi":         self.collecte("Noblesse")
      case "Évêque":      self.collecte("Religion")
      case "Marchand":    self.collecte("Commerce") ; +1 pièce d'or
      case "Architecte":  pioche 2 quartiers supplémentaires
      case "Condottiere": self.collecte("Soldatesque")
                          détruit éventuellement un quartier (coût − 1) d'une cité non complète,
                          sauf celle de l'Évêque

La revue fait apparaître les ajouts suivants :

  • assassinat et vol chez la victime. Des listes de 0 ou 1 personnage, testées au début de joue(personnage) et remises à zéro à chaque tour. On les range chez le player visé, pas chez celui qui agit.
  • Player.collecte(catégorie) et Cité.compte(catégorie). Le Roi, l’Évêque, le Marchand et le Condottiere perçoivent tous un revenu proportionnel à une catégorie de quartiers : le calcul se factorise.
  • Main.vide() pour l’échange de mains du Magicien, et Player.défausse(n) pour sa seconde option.
  • Cité.retire(quartier) et Cité.est_complète() pour la destruction par le Condottiere.
  • Partie.autre(player) pour désigner l’adversaire, utilisée par quatre rôles.

Pour aller plus loin

Le moment des actions

Prenez en compte le fait que certaines actions liées à un rôle peuvent s’exécuter en début, fin ou à n’importe quel moment du tour et compléter le pseudo-code des méthodes correspondantes.

Notez qu’il faut s’entendre sur ce que veut dire à n’importe quel moment du tour. La lecture de la règle semble dire qu’il ne s’agit pas d’agir hors du tour du personnage, mais d’avoir le choix d’agir, pendant son tour, au début, à la fin ou avant la construction, avec la contrainte qu’il ne peut y avoir qu’une seule collecte des revenus.

À faire

Complétez le pseudo-code pour tenir compte du moment où l’action d’un rôle peut avoir lieu.

Correction
Cliquez ici pour afficher la correction.

agit se divise en trois méthodes : agit_avant, agit_pendant (avant la construction) et agit_après. Player.joue les appelle autour de choisit() et de construit(n).

Quand une même action peut avoir lieu à plusieurs de ces moments — la collecte, par exemple — un booléen a_collecté indique qu’elle a déjà été faite, pour qu’elle ne soit proposée qu’une fois.

Ce booléen doit être associé au personnage et non au player : à deux, un player détient deux personnages, qui peuvent tous les deux collecter. C’est précisément ce qui motive la question suivante.

On fait par ailleurs l’hypothèse qu’agit_après ne peut pas annuler une fin de partie. Elle tient ici parce que le Condottiere ne peut pas détruire un quartier d’une cité complète — mais elle sera à revérifier à chaque personnage ajouté.

Déléguer le comportement aux personnages

On peut se demander si le comportement d’un.e joueur.euse.s, qui dépend de la personification en cours, ne pourrait pas plutôt être associé au personnage lui-même. Un joueur (en tant qu’objet) pourrait alors déléguer son comportement au personnage (en tant qu’objet) dont il endosse le rôle.

Esquissez les modifications à faire pour mettre en place cette idée.

À faire

Esquissez les modifications nécessaires pour que le personnage porte lui-même son comportement.

Correction
Cliquez ici pour afficher la correction.

Personnage sort de la hiérarchie des cartes et devient une classe de comportement, avec une sous-classe par rôle. Player.joue(p) se réduit alors à :

if self.mes_personnages.contient(p): return p.joue()
else:                                return False
Nom de classe Attributs Méthodes Objets créés de la classe Objets d’autres classes associés
Personnage (abstraite) player: Player (mis à jour à chaque choix)
partie: Partie
a_collecté: Booléen
joue() -> Booléen (enchaîne agit_avant, choisit, agit_pendant, construit, agit_après)
agit_avant(), agit_pendant(), agit_après() (ne font rien par défaut)
choisit(), construit(n), collecte(catégorie)
réinitialise() -> Aucun
Aucun (abstraite) player, partie, IHM
Assassin, Voleur, Magicien, Roi, Évêque, Marchand, Architecte, Condottiere (héritent de Personnage) Attributs spécifiques éventuels (par exemple cible: [Personnage] pour Assassin et Voleur) Redéfinition de agit_avant / agit_pendant / agit_après (et de construit pour l’Architecte) 1 objet par classe : assassin, voleur, …, condottiere, créés par Partie, qui leur passe self. Ceux de Personnage

Par exemple :

CLASS Marchand INHERITS Personnage:
  METHOD agit_avant() -> Aucun:
    self.partie.trésor = self.partie.trésor - 1
    self.player.trésor = self.player.trésor + 1
    self.propose_collecte("Commerce")     # collecte si not a_collecté et si le player le souhaite
  METHOD agit_pendant() -> Aucun:
    self.propose_collecte("Commerce")
  METHOD agit_après() -> Aucun:
    self.propose_collecte("Commerce")

Cette organisation sépare ce que tous les personnages ont en commun, dans Personnage, de ce qui est propre à chaque rôle, dans les sous-classes. Ajouter un personnage revient alors à ajouter une sous-classe, sans toucher à Player.

Pour aller encore plus loin

Extensions possibles

Il est notamment possible de :

  • terminer la conception, initiale ou avancée ;
  • l’étendre pour prendre en compte :
    • les cartes merveilles ;
    • de nouveaux personnages ;
  • passer du pseudo-code au code pour vérifier la cohérence de la conception et obtenir une application exécutable ;
  • (avancé) faire évoluer la conception et le code pour créer un joueur virtuel autonome dans ses décisions ;
  • (avancé) faire évoluer la conception et le code pour supporter une interface graphique et donner la possibilité à 2 utilisateurs (ou plus) d’interagir chacun sur sa machine.

Annexes - Distribution des cartes de Quartier

Important

Notez que ces informations et ces textes, relatifs à la seconde édition du jeu, éditée par Ubik en 2003, sont protégés. J’ai rajouté l’émoji.

  • 12 cartes Évêque

    • 3 temples (coût 1 pièce d’or)
    • 3 églises (2)
    • 4 monastères (3)
    • 2 cathédrales (5)
  • 12 cartes Condottiere

    • 3 tours de guet (1)
    • 3 prisons (2)
    • 3 casernes (3)
    • 3 forteresses (5)
  • 21 carte Commerce

    • 5 tavernes (1)
    • 4 échoppes (2)
    • 4 marchés (2)
    • 3 comptoirs (3)
    • 3 ports (4)
    • 2 hôtels de ville (5)
  • 12 cartes Noblesse

    • 5 manoirs (3)
    • 2 palais (5)
    • 5 châteaux (4)
  • 14 cartes Merveille

    • 1 carte coûtant 2 pièces d’or
      • Cour des miracles : Pour le décompte final des points, la Cour des miracles est considérée comme un quartier de la couleur de votre choix. Vous ne pouvez pas utiliser cette capacité si vous avez construit la Cour des miracles au dernier tour de jeu.
    • 1 carte coûtant 3 pièces d’or
      • Donjon : Le donjon ne peut pas être détruit par le Condottiere.
    • 1 carte coûtant 4 pièces d’or
      • Trésor impérial : Le trésor impérial coûte 4 pièces d’or à bâtir, mais il rapporte autant de points que vous aurez de pièces d’or en votre possession en fin de partie.
    • 6 cartes coûtant 5 pièces d’or
      • Carrière : Vous pouvez bâtir un bâtiment de même nom que l’un de vos bâtiments déjà en jeu. Vous ne perdez pas les bâtiments ainsi construits si la carrière est détruite.
      • Cimetière : Lorsque le Condottiere détruit un quartier, vous pouvez payer une pièce d’or pour le prendre dans votre main. Vous ne pouvez pas faire cela si vous êtes vous-même Condottiere.
      • Fontaine aux souhaits : En fin de partie, la Fontaine outre les 5 points de victoire correspondant à son coût, rapport 1 point de victoire par autre bâtiment violet en votre possession.
      • Laboratoire : Une fois par tour, vous pouvez vous défausser d’une carte quartier de votre main et recevoir une pièce d’or en contrepartie.
      • Manufacture : Une fois par tour, vous pouvez payer deux pièces d’or pour piocher trois cartes.
      • Observatoire : Si vous choisissez de piocher des cartes au début de votre tour, vous en piochez trois, en choisissez une et défaussez les deux autres.
    • 5 cartes coûtant 6 pièces d’or
      • Bibliothèque : Si vous choisissez de piocher des cartes au début de votre tour, vous en piochez deux et les conservez toutes les deux.
      • Dracoport : Cette réalisation de prestige - on n’a pas vu de dragon dans le royaume depuis bientôt mille ans - coûte six pièces d’or à bâtir mais vaut huit points dans le décompte de fin de partie.
      • École de Magie : Pour la perception des revenus, l’École de Magie est considérée comme un quartier de la couleur de votre choix. Elle vous rapporte donc si vous êtes Roi, Évêque, Marchand ou Condottiere.
      • Grande Muraille : Le prix à payer par le Condottiere pour détruire vos autres quartiers est augmenté de 1.
      • Université : Cette réalisation de prestige - nul n’a jamais bien compris à quoi pouvait servir une université 😉 - coûte six pièces d’or à bâtir mais vaut huit points dans le décompte de fin de partie.