Moteur de cartes en Java

Tout cela a commencé lorsque nous avons remarqué qu'il y avait très peu d'applications de jeux de cartes ou d'applets écrits en Java. Nous avons d'abord pensé à écrire quelques jeux, et avons commencé par déterminer le code de base et les classes nécessaires pour créer des jeux de cartes. Le processus se poursuit, mais il existe maintenant un cadre assez stable à utiliser pour créer diverses solutions de jeux de cartes. Nous décrivons ici comment ce framework a été conçu, comment il fonctionne, ainsi que les outils et astuces qui ont été utilisés pour le rendre utile et stable.

Phase de conception

Avec la conception orientée objet, il est extrêmement important de connaître le problème de fond en comble. Sinon, il est possible de passer beaucoup de temps à concevoir des classes et des solutions qui ne sont pas nécessaires ou qui ne fonctionneront pas en fonction de besoins spécifiques. Dans le cas des jeux de cartes, une approche consiste à visualiser ce qui se passe lorsqu'une, deux ou plusieurs personnes jouent aux cartes.

Un jeu de cartes contient généralement 52 cartes de quatre couleurs différentes (diamants, cœurs, clubs, piques), avec des valeurs allant de deux au roi, plus l'as. Immédiatement, un problème se pose: selon les règles du jeu, les as peuvent être soit la valeur de carte la plus basse, soit la plus élevée, ou les deux.

De plus, il y a des joueurs qui prennent des cartes du deck dans une main et gèrent la main selon des règles. Vous pouvez soit montrer les cartes à tout le monde en les plaçant sur la table, soit les regarder en privé. Selon l'étape particulière du jeu, vous pouvez avoir N nombre de cartes en main.

L'analyse des étapes de cette manière révèle divers modèles. Nous utilisons maintenant une approche basée sur les cas, comme décrit ci-dessus, qui est documentée dans le génie logiciel orienté objet d' Ivar Jacobson . Dans ce livre, l'une des idées de base est de modéliser des classes basées sur des situations réelles. Cela permet de comprendre beaucoup plus facilement comment les relations fonctionnent, ce qui dépend de quoi et comment les abstractions fonctionnent.

Nous avons des classes telles que CardDeck, Hand, Card et RuleSet. Un CardDeck contiendra 52 objets Card au début, et CardDeck aura moins d'objets Card car ceux-ci sont dessinés dans un objet Hand. Les objets de la main parlent avec un objet RuleSet qui a toutes les règles concernant le jeu. Considérez un RuleSet comme le manuel du jeu.

Classes vectorielles

Dans ce cas, nous avions besoin d'une structure de données flexible qui gère les changements d'entrée dynamiques, ce qui a éliminé la structure de données Array. Nous voulions également un moyen simple d'ajouter un élément d'insertion et d'éviter si possible beaucoup de codage. Il existe différentes solutions disponibles, telles que diverses formes d'arbres binaires. Cependant, le package java.util a une classe Vector qui implémente un tableau d'objets dont la taille augmente et diminue si nécessaire, ce qui était exactement ce dont nous avions besoin. (Les fonctions membres Vector ne sont pas entièrement expliquées dans la documentation actuelle; cet article expliquera plus en détail comment la classe Vector peut être utilisée pour des instances de liste d'objets dynamiques similaires.) L'inconvénient des classes Vector est l'utilisation de mémoire supplémentaire, en raison de beaucoup de mémoire copie effectuée dans les coulisses. (Pour cette raison, les tableaux sont toujours meilleurs; ils sont de taille statique,afin que le compilateur puisse trouver des moyens d'optimiser le code). De plus, avec de plus grands ensembles d'objets, nous pourrions avoir des pénalités concernant les temps de recherche, mais le plus grand vecteur auquel nous pouvions penser était 52 entrées. Cela reste raisonnable dans ce cas, et les longs temps de recherche n'étaient pas un problème.

Une brève explication de la façon dont chaque classe a été conçue et mise en œuvre suit.

Classe de carte

La classe Card est très simple: elle contient des valeurs signalant la couleur et la valeur. Il peut également avoir des pointeurs vers des images GIF et des entités similaires qui décrivent la carte, y compris un comportement simple possible tel que l'animation (retourner une carte) et ainsi de suite.

class Card implémente CardConstants {public int color; public int value; public String ImageName; }

Ces objets Card sont ensuite stockés dans différentes classes Vector. Notez que les valeurs des cartes, y compris la couleur, sont définies dans une interface, ce qui signifie que chaque classe du framework pourrait implémenter et de cette façon inclure les constantes:

interface CardConstants {// Les champs d'interface sont toujours publics statiques finaux! int HEARTS 1; int DIAMOND 2; int SPADE 3; int CLUBS 4; int JACK 11; int QUEEN 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; }

Classe CardDeck

La classe CardDeck aura un objet Vector interne, qui sera pré-initialisé avec 52 objets carte. Cela se fait à l'aide d'une méthode appelée shuffle. L'implication est que chaque fois que vous mélangez, vous démarrez effectivement une partie en définissant 52 cartes. Il est nécessaire de supprimer tous les anciens objets possibles et de recommencer à partir de l'état par défaut (52 objets carte).

public void shuffle () {// Toujours remettre à zéro le vecteur de deck et l'initialiser à partir de zéro. deck.removeAllElements (); 20 // Puis insérez les 52 cartes. Une couleur à la fois pour (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color HEARTS; aCard.value i; deck.addElement (aCard); } // Faites de même pour les CLUBS, DIAMANTS et SPADES. }

Lorsque nous dessinons un objet Card à partir du CardDeck, nous utilisons un générateur de nombres aléatoires qui connaît l'ensemble dans lequel il choisira une position aléatoire à l'intérieur du vecteur. En d'autres termes, même si les objets Card sont ordonnés, la fonction aléatoire choisira une position arbitraire dans la portée des éléments à l'intérieur du Vector.

Dans le cadre de ce processus, nous supprimons également l'objet réel du vecteur CardDeck lorsque nous passons cet objet à la classe Hand. La classe Vector cartographie la situation réelle d'un jeu de cartes et d'une main en passant une carte:

public Card draw () {Card aCard null; int position (int) (Math.random () * (deck.size = ())); essayez {aCard (Card) deck.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (position); retourner une carte; }

Notez qu'il est bon de détecter toutes les exceptions possibles liées à la prise d'un objet du vecteur à partir d'une position qui n'est pas présente.

Il existe une méthode utilitaire qui parcourt tous les éléments du vecteur et appelle une autre méthode qui videra une chaîne de paire valeur / couleur ASCII. Cette fonction est utile lors du débogage des classes Deck et Hand. Les fonctionnalités d'énumération des vecteurs sont beaucoup utilisées dans la classe Hand:

public void dump () {Énumération enum deck.elements (); while (enum.hasMoreElements ()) {Card card (Card) enum.nextElement (); RuleSet.printValue (carte); }}

Classe de main

La classe Hand est un véritable bourreau de travail dans ce cadre. La plupart des comportements requis étaient quelque chose de très naturel à placer dans cette classe. Imaginez des gens tenant des cartes dans leurs mains et effectuant diverses opérations tout en regardant les objets Card.

Tout d'abord, vous avez également besoin d'un vecteur, car dans de nombreux cas, on ne sait pas combien de cartes seront ramassées. Bien que vous puissiez implémenter un tableau, il est également bon d'avoir une certaine flexibilité ici. La méthode la plus naturelle dont nous avons besoin est de prendre une carte:

public void take (Card theCard) {cardHand.addElement (theCard); }

CardHandest un vecteur, donc nous ajoutons simplement l'objet Card dans ce vecteur. Cependant, dans le cas des opérations de "sortie" de la main, nous avons deux cas: l'un dans lequel nous montrons la carte, et l'autre dans lequel nous montrons et tirons tous les deux la carte de la main. Nous devons implémenter les deux, mais en utilisant l'héritage, nous écrivons moins de code car dessiner et montrer une carte est un cas particulier de simplement montrer une carte:

public Card show (position int) {Card aCard null; essayez {aCard (Card) cardHand.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } return aCard; } 20 tirage de cartes publiques (position int) {Show aCard (position); cardHand.removeElementAt (position); retourner une carte; }

En d'autres termes, le cas de dessin est un cas d'exposition, avec le comportement supplémentaire de supprimer l'objet du vecteur Main.

En écrivant du code de test pour les différentes classes, nous avons trouvé un nombre croissant de cas dans lesquels il était nécessaire de se renseigner sur diverses valeurs spéciales dans la main. Par exemple, nous avions parfois besoin de savoir combien de cartes d'un type spécifique se trouvaient dans la main. Ou la valeur basse par défaut de un a dû être changée en 14 (valeur la plus élevée) et inversement. Dans tous les cas, le soutien du comportement a été délégué à la classe Hand, car c'était un endroit très naturel pour un tel comportement. Encore une fois, c'était presque comme si un cerveau humain était derrière la main pour faire ces calculs.

La fonction d'énumération des vecteurs peut être utilisée pour savoir combien de cartes d'une valeur spécifique étaient présentes dans la classe Hand:

 public int NCards (int value) { int n 0; Enumeration enum cardHand.elements (); while (enum.hasMoreElements ()) { tempCard (Card) enum.nextElement (); // = tempCard defined if (tempCard.value= value) n++; } return n; } 

Similarly, you could iterate through the card objects and calculate the total sum of cards (as in the 21 test), or change the value of a card. Note that, by default, all objects are references in Java. If you retrieve what you think is a temporary object and modify it, the actual value is also changed inside the object stored by the vector. This is an important issue to keep in mind.

RuleSet class

The RuleSet class is like a rule book that you check now and then when you play a game; it contains all the behavior concerning the rules. Note that the possible strategies a game player may use are based either on user interface feedback or on simple or more complex artificial intelligence (AI) code. All the RuleSet worries about is that the rules are followed.

Other behaviors related to cards were also placed into this class. For example, we created a static function that prints the card value information. Later, this could also be placed into the Card class as a static function. In the current form, the RuleSet class has just one basic rule. It takes two cards and sends back information about which card was the highest one:

 public int higher (Card one, Card two) { int whichone 0; if (one.value= ACE_LOW) one.value ACE_HIGH; if (two.value= ACE_LOW) two.value ACE_HIGH; // In this rule set the highest value wins, we don't take into // account the color. if (one.value > two.value) whichone 1; if (one.value < two.value) whichone 2; if (one.value= two.value) whichone 0; // Normalize the ACE values, so what was passed in has the same values. if (one.value= ACE_HIGH) one.value ACE_LOW; if (two.value= ACE_HIGH) two.value ACE_LOW; return whichone; } 

You need to change the ace values that have the natural value of one to 14 while doing the test. It's important to change the values back to one afterward to avoid any possible problems as we assume in this framework that aces are always one.

In the case of 21, we subclassed RuleSet to create a TwentyOneRuleSet class that knows how to figure out if the hand is below 21, exactly 21, or above 21. It also takes into account the ace values that could be either one or 14, and tries to figure out the best possible value. (For more examples, consult the source code.) However, it's up to the player to define the strategies; in this case, we wrote a simple-minded AI system where if your hand is below 21 after two cards, you take one more card and stop.

How to use the classes

It is fairly straightforward to use this framework:

 myCardDeck new CardDeck (); myRules new RuleSet (); handA new Hand (); handB new Hand (); DebugClass.DebugStr ("Draw five cards each to hand A and hand B"); for (int i 0; i < NCARDS; i++) { handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // Test programs, disable by either commenting out or using DEBUG flags. testHandValues (); testCardDeckOperations(); testCardValues(); testHighestCardValues(); test21(); 

The various test programs are isolated into separate static or non-static member functions. Create as many hands as you want, take cards, and let the garbage collection get rid of unused hands and cards.

You call the RuleSet by providing the hand or card object, and, based on the returned value, you know the outcome:

 DebugClass.DebugStr ("Compare the second card in hand A and Hand B"); int winner myRules.higher (handA.show (1), = handB.show (1)); if (winner= 1) o.println ("Hand A had the highest card."); else if (winner= 2) o.println ("Hand B had the highest card."); else o.println ("It was a draw."); 

Or, in the case of 21:

 int result myTwentyOneGame.isTwentyOne (handC); if (result= 21) o.println ("We got Twenty-One!"); else if (result > 21) o.println ("We lost " + result); else { o.println ("We take another card"); // ... } 

Testing and debugging

Il est très important d'écrire du code de test et des exemples lors de l'implémentation du framework réel. De cette façon, vous savez à tout moment à quel point le code d'implémentation fonctionne; vous réalisez des faits sur les fonctionnalités et des détails sur la mise en œuvre. Avec plus de temps, nous aurions implémenté le poker - un tel cas de test aurait fourni encore plus d'informations sur le problème et aurait montré comment redéfinir le cadre.