Двигател на карти в Java

Всичко започна, когато забелязахме, че има много малко приложения за игра на карти или аплети, написани на Java. Първо помислихме да напишем няколко игри и започнахме с измислянето на основния код и класовете, необходими за създаването на игри с карти. Процесът продължава, но сега има доста стабилна рамка, която да се използва за създаване на различни решения за игра на карти. Тук описваме как е проектирана тази рамка, как работи и инструментите и триковете, които са били използвани, за да я направят полезна и стабилна.

Фаза на проектиране

При обектно-ориентирания дизайн е изключително важно да се знае проблемът отвътре и отвън. В противен случай е възможно да отделите много време за проектиране на класове и решения, които не са необходими или няма да работят според специфичните нужди. В случай на игри с карти, един подход е да се визуализира какво се случва, когато едно, две или повече лица играят карти.

Колода от карти обикновено съдържа 52 карти в четири различни масти (диаманти, сърца, бухалки, пики), със стойности, вариращи от двойка до краля, плюс асото. Веднага възниква проблем: в зависимост от правилата на играта асовете могат да бъдат или най-ниската стойност на картата, най-високата, или и двете.

Освен това има играчи, които вземат карти от тестето в ръка и управляват ръката въз основа на правила. Можете или да покажете картите на всички, като ги поставите на масата, или да ги разгледате насаме. В зависимост от конкретния етап на играта, може да имате N брой карти в ръката си.

Анализът на етапите по този начин разкрива различни модели. Сега използваме подход, основан на казуси, както е описано по-горе, който е документиран в Обектно-ориентираното софтуерно инженерство на Ивар Якобсън . В тази книга една от основните идеи е да се моделират класове въз основа на ситуации от реалния живот. Това прави много по-лесно да се разбере как функционират отношенията, какво зависи от какво и как функционират абстракциите.

Имаме класове като CardDeck, Hand, Card и RuleSet. CardDeck ще съдържа 52 обекта Card в началото и CardDeck ще има по-малко обекти Card, тъй като те са изтеглени в обект Hand. Ръчните предмети говорят с обект RuleSet, който има всички правила относно играта. Помислете за RuleSet като наръчник за игра.

Векторни класове

В този случай се нуждаехме от гъвкава структура на данните, която обработва динамични промени в записа, което елиминира структурата на данните Array. Също така искахме лесен начин за добавяне на елемент за вмъкване и избягване на много кодиране, ако е възможно. Налични са различни решения, като например различни форми на двоични дървета. Пакетът java.util обаче има клас Vector, който реализира масив от обекти, които нарастват и намаляват по размер, както е необходимо, което беше точно това, от което се нуждаехме. (Функциите на член Vector не са напълно обяснени в текущата документация; тази статия ще обясни допълнително как класът Vector може да се използва за подобни екземпляри от динамичен списък с обекти.) Недостатъкът при класовете Vector е допълнително използване на паметта, поради много памет копиране, направено зад кулисите. (Поради тази причина масивите са винаги по-добри; те са със статичен размер,за да може компилаторът да намери начини за оптимизиране на кода). Също така, при по-големи набори обекти може да имаме наказания по отношение на времето за търсене, но най-големият вектор, за който можехме да се сетим, беше 52 записа. Това все още е разумно за този случай и дългите времена за търсене не бяха проблем.

Следва кратко обяснение как е проектиран и изпълнен всеки клас.

Клас на картата

Класът Card е много прост: той съдържа стойности, сигнализиращи за цвета и стойността. Той може също да има указатели към GIF изображения и подобни обекти, които описват картата, включително възможно просто поведение като анимация (обърнете карта) и така нататък.

class Card реализира CardConstants {public int color; публична int стойност; публичен низ ImageName; }

След това тези обекти на Card се съхраняват в различни класове Vector. Имайте предвид, че стойностите за картите, включително цвета, са дефинирани в интерфейс, което означава, че всеки клас в рамката може да внедри и по този начин включва константите:

интерфейс CardConstants {// полетата на интерфейса винаги са публични статични окончателни! int СЪРЦА 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; }

CardDeck клас

Класът CardDeck ще има вътрешен Vector обект, който ще бъде инициализиран предварително с 52 обекта от карта. Това се прави с помощта на метод, наречен разбъркване. Изводът е, че всеки път, когато разбърквате, вие наистина започвате игра, като дефинирате 52 карти. Необходимо е да премахнете всички възможни стари обекти и да започнете отново от състоянието по подразбиране (52 обекта на картата).

public void shuffle () {// Винаги нулирайте палубния вектор и го инициализирайте от нулата. deck.removeAllElements (); 20 // След това поставете 52-те карти. Един цвят наведнъж за (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color СЪРЦА; aCard.value i; deck.addElement (aCard); } // Направете същото за CLUBS, DIAMONDS и SPADES. }

Когато изтегляме обект Card от CardDeck, използваме генератор на случайни числа, който знае набора, от който ще избере произволна позиция във вектора. С други думи, дори ако обектите Card са подредени, произволната функция ще избере произволна позиция в обхвата на елементите във вектора.

Като част от този процес премахваме и действителния обект от вектора CardDeck, докато предаваме този обект на класа Hand. Класът Vector картографира реалната ситуация на тесте карти и ръка, като подава карта:

публично теглене на карта () {Card aCard null; int позиция (int) (Math.random () * (deck.size = ())); опитайте {aCard (Card) deck.elementAt (позиция); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (позиция); върнете aCard; }

Имайте предвид, че е добре да уловите всички възможни изключения, свързани с вземането на обект от вектора от позиция, която не е налична.

There is a utility method that iterates through all the elements in the vector and calls another method that will dump an ASCII value/color pair string. This feature is useful when debugging both the Deck and the Hand classes. The enumeration features of vectors are used a lot in the Hand class:

 public void dump () { Enumeration enum deck.elements (); while (enum.hasMoreElements ()) { Card card (Card) enum.nextElement (); RuleSet.printValue (card); } } 

Hand class

The Hand class is a real workhorse in this framework. Most of the behavior required was something that was very natural to place in this class. Imagine people holding cards in their hands and doing various operations while looking at the Card objects.

First, you also need a vector, since it's unknown in many cases how many cards will be picked up. Although you could implement an array, it's good to have some flexibility here, too. The most natural method we need is to take a card:

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

CardHand is a vector, so we are just adding the Card object into this vector. However, in the case of the "output" operations from the hand, we have two cases: one in which we show the card, and one in which we both show and draw the card from the hand. We need to implement both, but using inheritance we write less code because drawing and showing a card is a special case from just showing a card:

 public Card show (int position) { Card aCard null; try { aCard (Card) cardHand.elementAt (position); } catch (ArrayIndexOutOfBoundsException e){ e.printStackTrace (); } return aCard; } 20 public Card draw (int position) { Card aCard show (position); cardHand.removeElementAt (position); return aCard; } 

In other words, the draw case is a show case, with the additional behavior of removing the object from the Hand vector.

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

Много е важно да пишете тестов код и примери, докато прилагате действителната рамка. По този начин вие по всяко време знаете колко добре работи кодът за внедряване; осъзнавате факти за функции и подробности за изпълнението. Като се има предвид повече време, щяхме да внедрим покер - такъв тестов случай би осигурил още по-голяма представа за проблема и би показал как да предефинираме рамката.