Imprimer
Catégorie : SAÉ - Développement IHM
Affichages : 13070

Préambule :


 

Le jeu qui sert de base à ce tutoriel a été inventé pour l'occasion mais s'inspire d'autres jeux similaires. C'est un jeu de placement où chaque joueur dispose de quatre pions rouge ou noir, numérotés de 1 à 4. Les pions doivent être posés dans les cases d'une grille de taille 3x3. Quand tous les pions sont posés, il y a donc une case libre.

Les règles de placement sont :

 

Ce principe de placement assure que tous les pions puissent être posés et que le trou sera soit dans la case centrale, soit dans un des 4 coins.

En fin de partie, le gagnant est :

Exemples :

  2/4  
 2/4 1 2/4
  2/4  

Le premier joueur à posé son premier pion (valeur 1) au centre : le deuxième joueur peut seulement poser son pion de valeur 2 ou 4 dans les cases marquées 2/4.

 

1/3        1/3 
  1  
 1/3   1/3

Le premier joueur à posé son premier pion (valeur 1) au centre : le deuxième joueur peut seulement poser son pion de valeur 1 ou 3 dans les cases marquées 1/3.

 

1   
    X
 X X

Le premier joueur à posé son premier pion (valeur 1) en haut à gauche : le deuxième joueur ne peut poser aucun pion dans les cases marquées d'un X => pas d'adjacence avec une case occupée.

 

1 2  
 4 1 4 
3 2 3

Il y a 2 pions rouges autour du trou et 0 noirs (case adjacentes par les côtés). Le joueur noir gagne.

 

1 2  
 4 1 4
3 2 3

Il y a 1 pion rouge et 1 pion noir autour du trou. Le joueur rouge gagne car son pion est plus petit.

 

1 2  
 4 1 2
3 4 3

Il y a 1 pion rouge et 1 pion noir autour du trou. Pas de gagnant car les 2 pions ont la même valeur.


 

L'objectif de de tutoriel est de reproduire les étapes essentielles dans le développement d'un jeu de plateau grâce à boardifier-console, pour ensuite les appliquer à un projet de plus grande envergure.

boardifier-console est un framework de développement Java. Il permet de faciliter le développement des jeux de plateau en mode texte. Il peut être considéré comme un spin-off du framework boardifier, qui lui est basé sur la bibliothèque graphique JavaFx pour créer des jeux de plateau en mode graphique. La caractéristique principale de ces 2 framework est de forcer l'utilisation du paradigme MVC strict pour développer un jeu, tout en assurant la possibilité de mettre en place des tests unitaires.

Ces contraintes ont l'inconvénient de multiplier le nombre de classes à écrire. Cependant, le code obtenu est plus facile à déboguer, tester, faire évoluer. De plus, cela permet de suivre un développement incrémental, en ajoutant petit à petit des fonctionnalités, voire dans certains cas, de développer plusieurs de ces fonctionnalités en parallèle sans que leur intégration soit complexe. Enfin, boardifier-console contient déjà la plupart des classes nécessaires pour faire tourner la "machinerie" interne de tout jeu basé sur une visualisation en mode texte, ce qui accélère notablement le développement.

 

Pour tout jeu, dont ce tutoriel, l'organisation du développement va suivre les étapes :

 

Pour ce tutoriel, tout le code à créer/ajouter est fourni. En revanche, les explications concernent uniquement pourquoi et comment fait-on telle chose avec boardifier-console. Les aspects plus algorithmiques liés au respect des règles du jeu ne seront pas expliqués (par ex, le code qui calcul qui gagne la partie).

 


 

 L'analyse se fait généralement à partir de l'expression des besoins d'un client. Dans le cas de ce tutoriel, vous êtes votre propre client qui demande simplement de pouvoir joueur au jeu "The Hole". Cette analyse permet de répertorier des fonctionnalités, qui sont indépendantes de la façon de les coder et avec quel langage/environnement. En revanche, certaines fonctionnalités vont apparaître à cause de la façon d'utiliser l'application = scénario d'utilisation. C'est pourquoi l'analyse doit aussi fixer ces scénario, qui sont généralement illustrés par des storyboards et des maquettes lorsqu'il y a des interfaces graphiques pour "naviguer" dans l'application.

Cependant, le fait que le jeu soit en mode texte implique des façons d'interagir avec le jeu complètement différentes de celles en mode graphique. Par exemple, en mode texte, la choix du type des joueurs et le lancement d'une partie, se fera sans via des questions affichées à l'écran et des réponses au clavier. Alors qu'en mode graphique, il y aurait une une fenêtre de dialogue, avec des cases à cocher, des boutons, etc.

La phase d'analyse doit donc absolument prendre en compte cette différence afin de ne pas produire un cahier des charges irréalisable.

Pour ce tutoriel, on suppose qu'après discussion avec nous-même, on établit un seul scénario global :

Parallèlement, on choisit d'avoir une maquette dans le style suivant :

Le scénario d'une partie est le suivant :

A partir de ces élément, on peut établir une liste synthétique des fonctionnalités et de sous-fonctionnalités dont elles dépendent :

Nous allons voir que grâce à boardifier-console, l'essentiel de ces fonctionnalités sont déjà partiellement écrites.

 


 

Exemple :

cd ~
mkdir -p code/Java/HoleConsole
cd code/Java/HoleConsole
mv ~/Téléchargements/holeconsole-canvas.tgz .
tar zxf holeconsole-canvas.tgz

 

tuto holeconsole 002


Avant de continuer dans le développement de "The Hole - console", il faut en connaître un peu plus sur boardifier-console, notamment sur la façon de structurer le code et qu'est-ce qu'il faut créer par soi-même.

Le paradigme d'architecture logicielle étant MVC (Modèle, Vue, Contrôleur), cela se traduit directement dans l'arborescence du projet. On retrouve donc :

Remarque : la plupart des classes de boardifier-console sont exactement les mêmes dans son "grand frère" boardifier. Cela permet de plus facilement migrer vers une version totalement graphique d'un jeu.

Partie modèle

Partie vue

 

Partie contrôle

 


 

D'après l'analyse, il faut créer au moins 4 classes spécifiques au jeu :

Utiliser boardifier-console ne change rien à ce constat, excepté le fait que ces 4 classes vont devoir hériter des classes définies dans boardifier-console, et que d'autres classes vont être nécessaires. Pour comprendre pourquoi et comment faire, il faut connaître la logique utilisée pour la partie modèle de boardifier-console. Pour synthétiser :

 

 

 

 

Reste à définir les attributs et méthodes de ces classes, ce qui doit normalement se faire AVANT de coder. Mais pour cela, il faudrait connaître un peu mieux boardifier-console. Nous allons donc sortir du cadre conseillé et finir la conception parallèlement au développement.

 


 

Bouger un pion est une action de jeu. Pour exécuter réellement cette action de jeu, il faut appeler les méthodes de ContainerElement, par exemple addElement() ou moveElement(). La partie contrôle ayant accès au modèle, le développeur peut très bien appeler lui-même ces méthodes. Cependant, ce n'est pas la façon de procéder dans boardifier-console.

La procédure consiste à :

ATTENTION :  vu que les classes d'action & animation sont exactement les mêmes dans boardifier-console et son grand frère boardifier, il existe 2 constructeurs possibles dans les classes d'action, dont un qui prend en paramètre un nom d'animation. En mode console, il va de soi qu'il n'y a pas d'animation possible donc il faut uniquement utiliser le constructeur sans animation.

En fait, l'ActionPlayer va appeler des méthodes précises des objets GameAction, qui elles-mêmes vont appeler les méthodes pour modifier le modèle (par ex, moveElement()).

 

Le problème de cette procédure est qu'elle nécessite de créer soi-même les actions nécessaires et l'ActionList. Mais pour que le résultat de ces actions soit correct, il faut respecter des contraintes imposées par boardifier-console. Par exemple, il est impossible de bouger directement un pion d'un conteneur à un autre. Il faut d'abord l'enlever du conteneur source pour ensuite l'ajouter au conteneur destination.

Pour éviter au programmeur de gérer ces contraintes, boardifier-console propose une classe ActionFactory, qui permet de générer directement des instances d'ActionList pour les 4 opérations courantes dans un jeu. Cette classe contient ainsi les méthodes :

A noter que la classe ActionList contient une méthode addAll() afin de concaténer deux ActionList, si nécessaire. C'est la cas par exemple dans les Dames, lorsqu'un pion prend un autre pion : il faut générer une ActionList pour le déplacement du pion et la concaténer avec une ActionList pour enlever le pion pris du stage.

L'avantage d'utiliser ces méthodes est encore plus marquant avec la version graphique de boardifier quand on utilise des animations pour représenter les déplacements des éléments. Dans ce cas, il faut calculer la position de destination des éléments et ces méthodes le font automatiquement.

 

Cette façon de procéder peut paraître alambiquée mais en pratique, elle évite beaucoup de copier/coller de code et propose une façon universelle d'exécuter des actions de jeu, quel que soit le jeu et que le joueur soit humain ou ordinateur.

Malgré l'existence de ActionFactory, voici quelques explications sur les classes d'action, au cas où vous décidiez d'en créer de nouvelles.


La super classe (dans le modèle) représentant les actions est GameAction mais ce sont ses sous-classes qui sont réellement utilisables. Dans chaque sous-classe on retrouve 2 méthodes essentielles :

boardifier-console et boardifier ne proposent que des d'actions centrées sur les jeux de plateaux à pion+dés. Il y a donc :

Si vous voulez créer d'autres types d'action, il faudra donc écrire une sous-classe de GameAction qui définit les 2 méthodes mentionnées.


Un objet Action n'est jamais directement utilisé par le développeur. Comme mentionné ci-dessus, une Action doit être mise dans une ActionList qui est ensuite utilisée par un ActionPlayer pour exécuter réellement les actions de jeu.

La classe ActionList est donc un simple conteneur d'objets Action. Sans trop rentrer dans les détails, la classe ActionList contient des méthodes permettant d'ajouter des Action, soit individuellement, soit en pack. Un pack n'est utile que lorsque plusieurs actions de jeu doivent avoir une animation simultanée, par exemple bouger 2 pions en même temps. Sinon, on peut se contenter d'utiliser la méthode addSingleAction().

ATTENTION : l'ordre dans lequel on ajoute les Action sera celui qui sera ensuite joué par l'ActionPlayer

ActionList contient également une méthode  setDoEndOfTurn. Si cet attribut vaut true, l'ActionPlayer qui joue l'ActionList donnera l'ordre à la partie contrôle de déclencher le passage au joueur suivant une fois que toutes les actions auront été jouées.


La classe ActionPlayer et sa méthode playActions() permet de "jouer" les actions d'une ActionList. Le comportement central de playActions() est :

 Remarque : dans le framework boardifier, la classe ActionPlayer n'est pas implémentée tout à fait de la même façon, mais s'utilise à l'identique.


  

Remarque : Il est conseillé de coder soi-même les modifications indiquées ci-dessous. Cependant, vous pouvez vous rendre à la fin de cet article pour trouver le lien de téléchargement de la solution complète.

 

Quelques commentaires sur la façon de remplir les squelettes :

 

 


 Pour la classe Pawn :

 

Pour la classe HoleBoard :

  • La méthode setValidCells(int number) qui va mettre à jour le tableau 2D de booléens reachableCells (hérité de ContainerElement).
  • reachableCells est automatiquement créé avec la même taille que la grille, donc 3x3 pour "The Hole".
  • L'objectif de la méthode est de mettre true dans les cases de reachableCells qui correspondent à une case valide si on voulait poser un pion dont la valeur est number.
  • Pour éviter d'avoir un code trop long, cette méthode appelle computeValidCells() pour obtenir la liste de ce cases.
 

 

Pour la classe HolePawnPot :

  • Il n'y a aucune fonctionnalité particulière. Le code du squelette est donc complet.

 

Pour la classe HoleStageModel :

  • Pour les méthodes, il faut créer des setters pour chacun des attributs afin que HoleStageFactory puisse les manipuler.
  • Cependant, il ne faut pas oublier que chaque élément du stage doit être ajouté dans les listes mentionnées auparavant, grâce aux méthodes addElement() et addGrid(). C'est pourquoi les setters doivent suivre un schéma bien particulier.
  • Par exemple, pour les setters de HoleBoard et blackPawns, cela donne :
    public void setBoard(HoleBoard board) {
        this.board = board;
        addContainer(board);
    }
    public void setBlackPawns(Pawn[] blackPawns) {
        this.blackPawns = blackPawns;
        for(int i=0;i<blackPawns.length;i++) {
            addElement(blackPawns[i]);
        }
    }

 

  • Il faut ensuite définir un ou plusieurs des 4 callbacks qui sont automatiquement appelés lors de certaines opérations et qui permettent d'influer sur l'état du jeu.
  • Par défaut, ces 4 callbacks ne font rien mais ils peuvent être changés grâce aux méthodes onSelectionChange(), onPutInContainer(), onRemoveFromContainer() et onMoveInContainer().
  • Pour faire simple, les 4 méthodes mentionnées ci-dessus prennent en paramètre une fonction lambda (ou fonction fléchée) qui définit les instructions a exécuter lorsque, respectivement, un élément a été sélectionné, un élément a été inséré/supprimé/déplacer dans un conteneur.
  • Pour "The Hole - console", il n'y a qu'une seule situation qui nécessite de changer le comportement nul par défaut : un pion est supprimé de son pool et mis dans la grille 3x3, ce qui implique de vérifier si tous les pions ont été placés et que la partie est terminée.
  • Il faut donc utiliser onPutInGrid() pour changer le callback associé.
  • On peut par exemple le faire dans une méthode setupCallbacks() que l'on appelle à la fin du constructeur :
    private void setupCallbacks() { 
        onPutInContainer( (element, containerDest, rowDest, colDest) -> {
            if (containerDest != board) return; // if not put in board, do nothing
            Pawn p = (Pawn) element;
            if (p.getColor() == 0) {
                blackPawnsToPlay--;
            }
            else {
                redPawnsToPlay--;
            }
            if ((blackPawnsToPlay == 0) && (redPawnsToPlay == 0)) {
                computePartyResult();
            }
        });
    }
  •  Reste à définir la méthode computePartyResult() qui va calculer qui gagne et mettre fin à la partie :
    private void computePartyResult() {
        int idWinner = -1;
        // compute winner
        // ... to fulfill
        
        // set the winner
        model.setIdWinner(idWinner);
        // stop de the stage
        model.stopStage();
    }

 

Pour la classe HoleStageFactory :

  •  Il suffit de compléter la méthode setup() pour qu'elle crée tous les éléments déclarés dans HoleStageModel et les assigne grâce aux différents setters.
  • Par exemple :
    public void setup() {
        // create the text , top-left char is in (0,0)
        TextElement text = new TextElement(stageModel.getCurrentPlayerName(), stageModel);
        text.setLocation(0,0);
        stageModel.setPlayerName(text);
        // create the board, top-left char is in (0,1)
        stageModel.setBoard(new HoleBoard(0, 1, stageModel));
        //create the black pot, top-left char is in (18,0)
        HolePawnPot blackPot = new HolePawnPot(18,0, stageModel);
        stageModel.setBlackPot(blackPot);
        // create the black pawns
        Pawn[] blackPawns = new Pawn[4];
        for(int i=0;i<4;i++) {
            blackPawns[i] = new Pawn(i + 1, Pawn.PAWN_BLACK, stageModel);
        }
        stageModel.setBlackPawns(blackPawns);
        // assign black pawns to their pot
        for (int i=0;i<4;i++) {
            blackPot.addElement(blackPawns[i], i,0);
        }

        // to fulfill for the red pot in (25,0), and red pawns 
        // ...
    }

 

Remarques :

  • Dans l'exemple ci-dessus, on positionne le texte, le panneau 3x3, les pots, ... comme si on avait une zone de pixels, mais avec des caractères au lieu de pixel. Par exemple, les coordonnées (18,0) représentent la colonne 18 et ligne 0 dans cette zone.
  • A noter que les coordonnées des éléments qui sont (dé)placés dans un conteneur n'ont par défaut pas besoin d'être spécifiées. En effet, quand on utilise une action PutInContainerAction sur un élément, ou bien que l'on utilise addElement() pour mettre cet élément dans une conteneur, celui-ci va être mis par défaut dans un état particulier. Quand la partie contrôle doit mettre à jour le visuel, elle vérifie si des éléments sont dans cet état particulier, et si c'est le cas, elle calcule automatiquement leur position dans la case du conteneur. C'est ce qui se passe pour les pions de "The Hole - console".

 


 

Contrairement aux classes créées précédemment, la prise de décision fait partie du contrôle. Cependant, dans boardifier-console, elle peut être écrite sans que le reste de l’application soit écrit, notamment le contrôle global et la vue. On peut même l'implémenter en parallèle des classes du modèle, à partir du moment où l'on a bien défini toutes les zones du jeu, donc tous les objets ContainerElement nécessaires.

Pour implémenter la prise de décision, il suffit de créer une sous-classe de Decider et de définir sa méthode decide(). Le fichier de la classe doit aller dans le répertoire control.

Pour ce tutoriel, on doit donc compléter decide() dans le fichier HoleDecider.java

 

L'objectif de cette méthode est simplement de choisir un pion et sa destination, puis de représenter son déplacement par une instance de MoveAction, qui sera placée dans l'ActionList. Le choix du pion et de sa destination représente le plus gros challenge algorithmique si l'on veut le faire intelligemment. Dans ce tutoriel, pour simplifier, on prend une solution "au hasard" : on prend le premier pion disponible qui peut être placé et on choisit au hasard une des cases valides.

Cela conduit au code suivant (à compléter) :

    public ActionList decide() {
        ActionList actions = null;
        // do a cast get a variable of the real type to get access to the attributes of HoleStageModel
        HoleStageModel stage = (HoleStageModel)model.getGameStage();
        HoleBoard board = stage.getBoard(); // get the board
        HolePawnPot pot = null; // the pot where to take a pawn
        GameElement pawn = null; // the pawn that is moved
        int rowDest = 0; // the dest. row in board
        int colDest = 0; // the dest. col in board

        // fulfill below to set pot, pawn, rowDest, colDest
        // NB : the pot (black or red) depends on the player's id
        // recall: board.computeValidCells() allows to compute ... valid cells :-)
        // ...

        // create action list, with ActionFactory. After the last action, it is next player's turn.
        ActionList actions = ActionFactory.generatePutInContainer( model, pawn, "holeboard", rowDest, colDest);
        actions.setDoEndOfTurn(true); // after playing this action list, it will be the end of turn for current player.
        return actions;      
    }

 


 

Afin de respecter le paradigme MVC, boardifier-console repose sur les principes suivants  :

  • Chaque GameElement est associé à un ElementLook qui définit son aspect visuel (c.a.d. son "look"). Quand on définit une sous-classe de GameElement, il faut donc définir une sous-classe de ElementLook pour créer son look.
  • Comme on est en mode texte, le look est représenté par un tableau 2D de String, ce qui permet d'afficher des caractères spéciaux en couleur.
  • Le contenu de ce tableau 2D doit être définit dans une méthode render(), que chaque sous classe de ElementLook DOIT définir pour créer le rendu visuel de l'élément en fonction de son état.

Remarques :

  • render() est automatiquement appelé si l'état de l'élément a changé et que l'on a appelé addChangeFaceEvent()(cf. § sur GameElement). C'est pourquoi il est important d'écrire le code de render() pour qu'il tienne compte des états possibles de l'élément ... sauf pour certains états.
  • En effet, la gestion du changement de la visibilité de l'élément est automatisée. Il n'y a pas besoin de code spécial dans render() pour "effacer" son rendu visuel lorsque l'élément est invisible. C'est fait automatiquement.

 

Par exemple, pour créer le look des pions de "The Hole", PawnLook contient :

public class PawnLook extends ElementLook {

    public PawnLook(GameElement element) {
        super(element, 1, 1);
  }

  public protected render() {
        Pawn pawn = (Pawn)element; // just for convenience to avoid in the following lots of casts like: (Pawn)element
        if (pawn.getColor() == Pawn.PAWN_BLACK) {
            shape[0][0] = ConsoleColor.WHITE + ConsoleColor.BLACK_BACKGROUND + pawn.getNumber() + ConsoleColor.RESET;
        }
        else {
            shape[0][0] = ConsoleColor.BLACK + ConsoleColor.RED_BACKGROUND + pawn.getNumber() + ConsoleColor.RESET;
        }
    } 
}

Remarque : pour cet exemple, il y a juste besoin de définir le rendu visuel quand le pion est visible. Ce rendu ne changera jamais, contrairement aux dames, où il faudrait tester dans render() si le pion est une dame ou non afin de définir son visuel.

 

  • Pour le look des éléments "conteneurs" (c.a.d. de type ContainerElement, boardifier-console contient déjà une sous-classes de ElementLook, à savoir ContainerLook. Cette classe représente une table sans bordure, avec la possibilité d'avoir des cases jointes si le ContainerElement associé en a ou pas. La taille des cases peut être fixée ou bien être variable selon la taille des looks à l'intérieur. boardifier-console propose également 3 sous-classes plus spécialisée/optimisées  : 
    • GridLook, 
    • ClassicBoardLook
    • TableLook.
  • Les 2 premières servent uniquement lorsque le conteneur est vraiment une grille 2D, sans cases jointes. Le rendu visuel est une grille avec des cases de taille fixe, et avec ou sans bordure. La deuxième est juste une sous-classe de la première, afin d'avoir en plus une numérotation des lignes et colonnes.
  • La troisième classe permet de gérer une grille aussi bien avec que sans cases jointes, sans numérotation, avec une taille de case variable ou fixe.
  • A noter que s'il n'y a pas de cases jointes, il vaut mieux utiliser les 2 premières classes, plus performantes.
  • Ces classes gèrent elles-mêmes les looks des éléments se trouvant dans le conteneur, notamment leur position "locale".
  • Cela implique que dès qu'un élément est placé/déplacé/supprimé d'un conteneur, boardifer-console reproduit automatiquement la même action pour le look de l'élément au sein du look du conteneur. Le rendu visuel du conteneur est alors mis à jour, via l'appel de sa méthode render().
  • Pour résumer, grâce aux layouts, le développeur a juste besoin de gérer les modifications de la partie modèle, en déplaçant des éléments dans/hors de conteneurs, et boardifier-console gère automatiquement la mise à jour de la partie vue, à partir des modifications sur le modèle.

 

  • Puisque ces classes héritent de ElementLook, elles doivent définir la méthode render(). Pour les 3 sous-classes, on sépare le rendu des bordures et coordonnées du contenu, grâce aux méthodes :
    • renderInners() : recopie le tableau 2D de chaque look interne à la bonne position dans la tableau shape du look du conteneur.
    • renderBorders() : "dessine" les bordures de chaque case grâce à des caractères spéciaux, dans le tableau shape du look du conteneur.
    • renderCoords() : "dessine" les coordonnées de chaque ligne/colonne, dans le tableau shape du look du conteneur.
  • Si on veut créer une sous-classe, par exemple de GridLook, pour modifier la façon de faire le rendu visuel, il n'y a normalement aucune raison de redéfinir renderInners(). En revanche, on peut redéfinir render() et/ou renderBorders(). C'est ce qui est fait dans ce tutoriel avec la classe PawnPotLook.

 

 

  • Pour gérer tous les visuels d'un même stage, chaque sous-classe de GameStageModel doit avoir son équivalent pour la vue, sous la forme d'une sous-classe de GameStageView.
  • Cette classe n'a par elle-même aucun rôle d'affichage. C'est juste un conteneur pour tous les looks des éléments utilisés dans un même stage.
  • Toutes les sous-classes de GameStageView doivent redéfinir la méthode createLooks() afin de créer les looks des différents éléments du modèle.
  • Pour afficher réellement les looks stockés dans un GameStageView, boardifier-console utilise la classe RootPane qui représente une sorte de panneau qui peut être affiché à l'écran. RootPane contient une méthode update() qui parcourt tous les éléments à afficher afin de remplir ce panneau aux bonnes coordonnées texte.
  • Ce RootPane est lui-même inclut dans une View, qui gère le contenu de la fenêtre du jeu et notamment sa mise à jour lorsque le contrôleur le demande.
  • Dans boardifer-console, View est quasi une coquille vide. Cependant, elle permet d'avoir la même structuration de classe que dans boardifier, ce qui facilite le passage d'un jeu du mode texte à graphique.

 

Pour PawnPotLook, la redéfinition de la méthode renderBorders() permet de changer le visuel par défaut de la grille.

Enfin, pour HoleStageView, il suffit d'instancier les classes de look, avec les dimensions prévues par le maquettage, et d'utiliser addLook() pour ajouter ces instances au visuel du stage. Ce qui donne :

package view;
// imports
// ...
public class HoleStageView extends GameStageView {
    public HoleStageView(String name, GameStageModel gameStageModel) {
        super(name, gameStageModel);
    }

    @Override
    public void createLooks() {
        HoleStageModel model = (HoleStageModel)gameStageModel;

        // create a TextLook for the text element
        addLook(new TextLook(model.getPlayerName()));
        // create a ClassicBoardLook (with borders and coordinates) for the main board.
        addLook(new ClassicBoardLook(2, 4, model.getBoard(), 1, 1, true));
        // create looks for both pots
        addLook(new BlackPawnPotLook(model.getBlackPot()));
        addLook(new RedPawnPotLook(2, 4, model.getRedPot()));
        // create looks for all pawns
        for(int i=0;i<4;i++) {
            addLook(new PawnLook(model.getBlackPawns()[i]));
            addLook(new PawnLook(model.getRedPawns()[i]));
        }
    }
}

 


 

HoleController étant déjà complètement écrite, seuls quelques commentaires sont nécessaires.

  • La méthode stageLoop()  se contente de boucler jusqu'à la fin de partie, en appelant playTurn() et endOfTurn() pour alterner entre les joueurs, puis update() afin de mettre à jour le modèle et la vue, et ainsi afficher celle-ci.
  • playTurn() récupère le type du joueur courant et en fonction, crée un HoleDecider pour deviner le coup à jouer, soit demande des instructions au clavier. Dans ce dernier cas, analyseAndPlay() permet de vérifier et jouer le coup choisi par le joueur.
  • endOfTurn() utilise le modèle pour mettre à jour le joueur courant, grâce à la méthode par défaut qui produit simplement une alternance entre joueurs. 

 


 

La classe HoleConsole est en fait quasi complète. En effet, quel que soit le jeu, les étapes de création et initialisation des objets sont presque identiques. Il n'y a globalement que 3 étapes qui dépendent du jeu :

  • l'ajout des joueurs au modèle,
  • l'enregistrement des noms des classes associées aux stages dans le StageFactory,
  • quel stage doit être utilisé lorsqu'une partie commence.

Ensuite, il suffit de lancer la partie et entrer dans la boucle de gestion du stage.

Cela donne :

    public static void main(String[] args) {
        ...
        if (mode == 0) {
            model.addHumanPlayer("player1");
            model.addHumanPlayer("player2");
        }
        else if (mode == 1) {
            model.addHumanPlayer("player");
            model.addComputerPlayer("computer");
        }
        else if (mode == 2) {
            model.addComputerPlayer("computer1");
            model.addComputerPlayer("computer2");
        }

        StageFactory.registerModelAndView("hole", "model.HoleStageModel", "view.HoleStageView");
        View holeView = new View(model);
        ControllerHole control = new ControllerHole(model,holeView);
        control.setFirstStageName("hole");
        try {
            control.startGame();
            control.stageLoop();
        }
        catch(GameException e) {
            System.out.println("Cannot start the game. Abort");
        }
    }

 


  

 L'archive de la solution complète est téléchargeable [ ici ]. Elle contient un projet IDEA complet que vous devrez reconfigurer pour l'adapter à votre ordinateur.

En enlevant toute la partie boardifier-console, cela représente en gros 550 lignes de code, lignes vides/commentaires exclus, ce qui est vraiment peu pour un petit jeu parfaitement fonctionnel. S'il fallait l'écrire complètement, il faudrait multiplier ce nombre par environ 3, sachant que cela donnerait sans doute un code bien moins évolutif et peu réutilisable pour créer d'autre jeux.