Karten-Engine in Java

Dies alles begann, als wir bemerkten, dass nur sehr wenige Kartenspielanwendungen oder Applets in Java geschrieben waren. Zuerst haben wir uns überlegt, ein paar Spiele zu schreiben, und zunächst den Kerncode und die Klassen herausgefunden, die für die Erstellung von Kartenspielen erforderlich sind. Der Prozess geht weiter, aber jetzt gibt es ein ziemlich stabiles Framework für die Erstellung verschiedener Kartenspiellösungen. Hier beschreiben wir, wie dieses Framework entworfen wurde, wie es funktioniert und welche Tools und Tricks verwendet wurden, um es nützlich und stabil zu machen.

Design-Phase

Bei objektorientiertem Design ist es äußerst wichtig, das Problem in- und auswendig zu kennen. Andernfalls können Sie viel Zeit damit verbringen, Klassen und Lösungen zu entwerfen, die nicht benötigt werden oder nicht den spezifischen Anforderungen entsprechen. Bei Kartenspielen besteht ein Ansatz darin, zu visualisieren, was passiert, wenn eine, zwei oder mehr Personen Karten spielen.

Ein Kartenspiel enthält normalerweise 52 Karten in vier verschiedenen Farben (Diamanten, Herzen, Keulen, Pik) mit Werten zwischen Zwei und König sowie dem Ass. Sofort tritt ein Problem auf: Abhängig von den Spielregeln können die Asse entweder der niedrigste Kartenwert, der höchste oder beides sein.

Darüber hinaus gibt es Spieler, die Karten aus dem Stapel in eine Hand nehmen und die Hand nach Regeln verwalten. Sie können die Karten entweder allen zeigen, indem Sie sie auf den Tisch legen, oder sie privat ansehen. Abhängig von der jeweiligen Phase des Spiels haben Sie möglicherweise N Karten auf der Hand.

Die Analyse der Stufen auf diese Weise zeigt verschiedene Muster. Wir verwenden jetzt einen fallgesteuerten Ansatz, wie oben beschrieben, der in Ivar Jacobsons objektorientiertem Software-Engineering dokumentiert ist . In diesem Buch besteht eine der Grundideen darin, Klassen basierend auf realen Situationen zu modellieren. Das macht es viel einfacher zu verstehen, wie Beziehungen funktionieren, was von was abhängt und wie die Abstraktionen funktionieren.

Wir haben Klassen wie CardDeck, Hand, Card und RuleSet. Ein CardDeck enthält zu Beginn 52 Kartenobjekte, und CardDeck enthält weniger Kartenobjekte, da diese in ein Handobjekt gezeichnet werden. Handobjekte sprechen mit einem RuleSet-Objekt, das alle Spielregeln enthält. Stellen Sie sich einen Regelsatz als Spielhandbuch vor.

Vektorklassen

In diesem Fall benötigten wir eine flexible Datenstruktur, die dynamische Eintragsänderungen verarbeitet, wodurch die Array-Datenstruktur eliminiert wurde. Wir wollten auch eine einfache Möglichkeit, ein Einfügeelement hinzuzufügen und möglichst viel Codierung zu vermeiden. Es stehen verschiedene Lösungen zur Verfügung, z. B. verschiedene Formen von Binärbäumen. Das Paket java.util verfügt jedoch über eine Vector-Klasse, die ein Array von Objekten implementiert, deren Größe nach Bedarf zunimmt und abnimmt. Genau das haben wir benötigt. (Die Funktionen der Vektorelemente werden in der aktuellen Dokumentation nicht vollständig erläutert. In diesem Artikel wird weiter erläutert, wie die Vektorklasse für ähnliche Instanzen dynamischer Objektlisten verwendet werden kann.) Der Nachteil bei Vektorklassen ist die zusätzliche Speichernutzung aufgrund des hohen Arbeitsspeichers Kopieren hinter den Kulissen. (Aus diesem Grund sind Arrays immer besser; sie haben eine statische Größe.so konnte der Compiler Wege finden, um den Code zu optimieren). Bei größeren Objektgruppen kann es auch zu Strafen hinsichtlich der Suchzeiten kommen, aber der größte Vektor, an den wir denken konnten, waren 52 Einträge. Das ist für diesen Fall immer noch vernünftig, und lange Suchzeiten waren kein Problem.

Es folgt eine kurze Erklärung, wie jede Klasse entworfen und implementiert wurde.

Kartenklasse

Die Kartenklasse ist sehr einfach: Sie enthält Werte, die die Farbe und den Wert anzeigen. Es kann auch Zeiger auf GIF-Bilder und ähnliche Objekte enthalten, die die Karte beschreiben, einschließlich eines möglichen einfachen Verhaltens wie Animation (Umdrehen einer Karte) und so weiter.

Klasse Card implementiert CardConstants {public int color; öffentlicher int Wert; public String ImageName; }}

Diese Kartenobjekte werden dann in verschiedenen Vektorklassen gespeichert. Beachten Sie, dass die Werte für die Karten, einschließlich der Farbe, in einer Schnittstelle definiert sind. Dies bedeutet, dass jede Klasse im Framework implementiert werden kann und auf diese Weise die Konstanten enthält:

Schnittstelle CardConstants {// Schnittstellenfelder sind immer öffentlich statisch final! int HERZEN 1; int DIAMOND 2; int SPADE 3; int CLUBS 4; int JACK 11; int KÖNIGIN 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; }}

CardDeck-Klasse

Die CardDeck-Klasse verfügt über ein internes Vektorobjekt, das mit 52 Kartenobjekten vorinitialisiert wird. Dies erfolgt mit einer Methode namens Shuffle. Die Implikation ist, dass Sie jedes Mal, wenn Sie mischen, ein Spiel starten, indem Sie 52 Karten definieren. Es ist notwendig, alle möglichen alten Objekte zu entfernen und wieder vom Standardzustand (52 Kartenobjekte) zu beginnen.

public void shuffle () {// Den Deckvektor immer auf Null setzen und von Grund auf neu initialisieren. deck.removeAllElements (); 20 // Dann lege die 52 Karten ein. Jeweils eine Farbe für (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color HEARTS; aCard.value i; deck.addElement (aCard); } // Machen Sie dasselbe für CLUBS, DIAMONDS und SPADES. }}

Wenn wir ein Kartenobjekt aus dem CardDeck zeichnen, verwenden wir einen Zufallszahlengenerator, der die Menge kennt, aus der eine zufällige Position innerhalb des Vektors ausgewählt wird. Mit anderen Worten, selbst wenn die Kartenobjekte geordnet sind, wählt die Zufallsfunktion eine beliebige Position innerhalb des Bereichs der Elemente innerhalb des Vektors.

Als Teil dieses Prozesses entfernen wir auch das eigentliche Objekt aus dem CardDeck-Vektor, wenn wir dieses Objekt an die Hand-Klasse übergeben. Die Vector-Klasse bildet die reale Situation eines Kartenspiels und einer Hand durch Übergeben einer Karte ab:

public Card draw () {Karte aCard null; int position (int) (Math.random () * (deck.size = ())); try {aCard (Card) deck.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (Position); aCard zurückgeben; }}

Beachten Sie, dass es gut ist, mögliche Ausnahmen im Zusammenhang mit dem Entfernen eines Objekts aus dem Vektor von einer Position aus zu erfassen, die nicht vorhanden ist.

Es gibt eine Dienstprogrammmethode, die alle Elemente im Vektor durchläuft und eine andere Methode aufruft, die eine ASCII-Wert / Farbpaar-Zeichenfolge ausgibt. Diese Funktion ist nützlich, wenn Sie sowohl die Deck- als auch die Hand-Klasse debuggen. Die Aufzählungsmerkmale von Vektoren werden in der Hand-Klasse häufig verwendet:

public void dump () {Aufzählung enum deck.elements (); while (enum.hasMoreElements ()) {Card card (Card) enum.nextElement (); RuleSet.printValue (Karte); }}

Handklasse

Die Hand-Klasse ist in diesem Rahmen ein echtes Arbeitstier. Das meiste erforderliche Verhalten war etwas sehr Natürliches in dieser Klasse. Stellen Sie sich vor, Menschen halten Karten in ihren Händen und führen verschiedene Operationen aus, während sie die Kartenobjekte betrachten.

Zunächst benötigen Sie auch einen Vektor, da in vielen Fällen nicht bekannt ist, wie viele Karten aufgenommen werden. Obwohl Sie ein Array implementieren könnten, ist es auch hier gut, etwas Flexibilität zu haben. Die natürlichste Methode, die wir brauchen, ist eine Karte zu nehmen:

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

CardHandist ein Vektor, also fügen wir nur das Kartenobjekt in diesen Vektor ein. Im Fall der "Ausgabe" -Operationen von der Hand haben wir jedoch zwei Fälle: einen, in dem wir die Karte zeigen, und einen, in dem wir beide die Karte zeigen und aus der Hand ziehen. Wir müssen beides implementieren, aber mit Vererbung schreiben wir weniger Code, da das Zeichnen und Zeigen einer Karte ein Sonderfall ist, wenn nur eine Karte gezeigt wird:

public Card show (int position) {Karte aCard null; try {aCard (Card) cardHand.elementAt (position); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } return aCard; } 20 öffentliches Kartenziehen (int Position) {Card aCard show (Position); cardHand.removeElementAt (Position); aCard zurückgeben; }}

Mit anderen Worten, der Zeichenfall ist ein Schaukasten mit dem zusätzlichen Verhalten, das Objekt aus dem Handvektor zu entfernen.

In writing test code for the various classes, we found an increasing number of cases in which it was necessary to find out about various special values in the hand. For example, sometimes we needed to know how many cards of a specific type were in the hand. Or the default ace low value of one had to be changed into 14 (highest value) and back again. In every case the behavior support was delegated back into the Hand class, as it was a very natural place for such behavior. Again, it was almost as though a human brain was behind the hand doing these calculations.

The enumeration feature of vectors may be used to find out how many cards of a specific value were present in the Hand class:

 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

Es ist sehr wichtig, Testcode und Beispiele zu schreiben, während das eigentliche Framework implementiert wird. Auf diese Weise wissen Sie jederzeit, wie gut der Implementierungscode funktioniert. Sie erkennen Fakten über Funktionen und Details zur Implementierung. Mit mehr Zeit hätten wir Poker implementiert - ein solcher Testfall hätte noch mehr Einblick in das Problem gegeben und gezeigt, wie das Framework neu definiert werden kann.