Motore di scheda in Java

Tutto è iniziato quando abbiamo notato che c'erano pochissime applicazioni per giochi di carte o applet scritte in Java. Per prima cosa abbiamo pensato di scrivere un paio di giochi e abbiamo iniziato a capire il codice di base e le classi necessarie per creare giochi di carte. Il processo continua, ma ora esiste un framework abbastanza stabile da utilizzare per creare varie soluzioni di giochi di carte. Qui descriviamo come è stato progettato questo framework, come funziona e gli strumenti e i trucchi utilizzati per renderlo utile e stabile.

Fase di progettazione

Con il design orientato agli oggetti, è estremamente importante conoscere il problema dentro e fuori. In caso contrario, è possibile dedicare molto tempo alla progettazione di classi e soluzioni che non sono necessarie o non funzioneranno in base a esigenze specifiche. Nel caso dei giochi di carte, un approccio consiste nel visualizzare cosa succede quando una, due o più persone giocano a carte.

Un mazzo di carte di solito contiene 52 carte in quattro semi diversi (quadri, cuori, fiori, picche), con valori che vanno dal due al re, più l'asso. Immediatamente sorge un problema: a seconda delle regole del gioco, gli assi possono essere il valore della carta più basso, il più alto o entrambi.

Inoltre, ci sono giocatori che prendono le carte dal mazzo in una mano e gestiscono la mano in base alle regole. Puoi mostrare le carte a tutti mettendole sul tavolo o guardandole in privato. A seconda della fase particolare del gioco, potresti avere N numero di carte in mano.

Analizzare le fasi in questo modo rivela vari modelli. Ora utilizziamo un approccio basato sui casi, come descritto sopra, che è documentato in Object Oriented Software Engineering di Ivar Jacobson . In questo libro, una delle idee di base è modellare le classi sulla base di situazioni di vita reale. Ciò rende molto più facile capire come funzionano le relazioni, cosa dipende da cosa e come operano le astrazioni.

Abbiamo classi come CardDeck, Hand, Card e RuleSet. Un CardDeck conterrà 52 oggetti Card all'inizio, e CardDeck avrà meno oggetti Card in quanto vengono disegnati in un oggetto Mano. Gli oggetti mano parlano con un oggetto RuleSet che ha tutte le regole riguardanti il ​​gioco. Pensa a un RuleSet come al manuale del gioco.

Classi vettoriali

In questo caso, avevamo bisogno di una struttura dati flessibile che gestisse le modifiche alle voci dinamiche, eliminando la struttura dei dati Array. Volevamo anche un modo semplice per aggiungere un elemento insert ed evitare, se possibile, molta codifica. Sono disponibili diverse soluzioni, come varie forme di alberi binari. Tuttavia, il pacchetto java.util ha una classe Vector che implementa un array di oggetti che cresce e si restringe in base alle necessità, che era esattamente ciò di cui avevamo bisogno. (Le funzioni del membro Vector non sono completamente spiegate nella documentazione corrente; questo articolo spiegherà ulteriormente come la classe Vector può essere utilizzata per istanze di elenchi di oggetti dinamici simili.) Lo svantaggio delle classi Vector è l'uso aggiuntivo della memoria, a causa di molta memoria copia fatta dietro le quinte. (Per questo motivo, gli array sono sempre migliori; sono di dimensioni statiche,in modo che il compilatore possa trovare modi per ottimizzare il codice). Inoltre, con set di oggetti più grandi, potremmo avere delle penalità sui tempi di ricerca, ma il vettore più grande a cui potevamo pensare era di 52 voci. È ancora ragionevole per questo caso e i lunghi tempi di ricerca non erano un problema.

Segue una breve spiegazione di come ciascuna classe è stata progettata e implementata.

Classe di carte

La classe Card è molto semplice: contiene valori che segnalano il colore e il valore. Può anche contenere puntatori a immagini GIF e entità simili che descrivono la scheda, inclusi possibili comportamenti semplici come animazioni (capovolgere una scheda) e così via.

class Card implementa CardConstants {public int color; valore int pubblico; public String ImageName; }

Questi oggetti Card vengono quindi memorizzati in varie classi Vector. Si noti che i valori per le carte, incluso il colore, sono definiti in un'interfaccia, il che significa che ogni classe nel framework potrebbe implementare e in questo modo includere le costanti:

interfaccia CardConstants {// i campi dell'interfaccia sono sempre pubblici statici finali! int CUORI 1; int DIAMOND 2; int SPADE 3; int CLUB 4; int JACK 11; int QUEEN 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; }

Classe CardDeck

La classe CardDeck avrà un oggetto Vector interno, che sarà pre-inizializzato con 52 oggetti card. Questo viene fatto utilizzando un metodo chiamato shuffle. L'implicazione è che ogni volta che mischi, inizi effettivamente una partita definendo 52 carte. È necessario rimuovere tutti i vecchi oggetti possibili e ricominciare dallo stato di default (52 oggetti tessera).

public void shuffle () {// Azzera sempre il vettore del mazzo e inizializzalo da zero. deck.removeAllElements (); 20 // Quindi inserisci le 52 carte. Un colore alla volta per (int i ACE_LOW; i <ACE_HIGH; i ++) {Card aCard new Card (); aCard.color CUORI; aCard.value i; deck.addElement (aCard); } // Fai lo stesso per CLUB, DIAMANTI e PICCHE. }

Quando disegniamo un oggetto Card dal CardDeck, stiamo usando un generatore di numeri casuali che conosce l'insieme da cui selezionerà una posizione casuale all'interno del vettore. In altre parole, anche se gli oggetti Card sono ordinati, la funzione random sceglierà una posizione arbitraria nell'ambito degli elementi all'interno del Vector.

Come parte di questo processo, stiamo anche rimuovendo l'oggetto effettivo dal vettore CardDeck mentre passiamo questo oggetto alla classe Hand. La classe Vector mappa la situazione reale di un mazzo di carte e di una mano passando una carta:

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

Si noti che è bene rilevare eventuali eccezioni relative al prelievo di un oggetto dal vettore da una posizione che non è presente.

C'è un metodo di utilità che itera attraverso tutti gli elementi nel vettore e chiama un altro metodo che scaricherà una stringa di coppia valore / colore ASCII. Questa funzione è utile durante il debug di entrambe le classi Deck e Hand. Le caratteristiche di enumerazione dei vettori sono molto utilizzate nella classe Hand:

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

Classe di mano

La classe Hand è un vero cavallo di battaglia in questo framework. La maggior parte del comportamento richiesto era qualcosa che era molto naturale da inserire in questa classe. Immagina persone che tengono le carte in mano e fanno varie operazioni mentre guardano gli oggetti Carta.

Innanzitutto, è necessario anche un vettore, poiché in molti casi non si sa quante carte verranno raccolte. Sebbene tu possa implementare un array, è bene avere una certa flessibilità anche qui. Il metodo più naturale di cui abbiamo bisogno è prendere una carta:

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

CardHandè un vettore, quindi stiamo solo aggiungendo l'oggetto Carta a questo vettore. Tuttavia, nel caso delle operazioni di "uscita" dalla mano, abbiamo due casi: uno in cui mostriamo la carta, e uno in cui entrambi mostriamo e pesciamo la carta dalla mano. Dobbiamo implementare entrambi, ma usando l'ereditarietà scriviamo meno codice perché disegnare e mostrare una carta è un caso speciale rispetto al semplice mostrare una carta:

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

In altre parole, il draw case è uno show case, con il comportamento aggiuntivo di rimuovere l'oggetto dal vettore Hand.

Scrivendo il codice di test per le varie classi, abbiamo riscontrato un numero crescente di casi in cui era necessario conoscere vari valori speciali nella mano. Ad esempio, a volte avevamo bisogno di sapere quante carte di un tipo specifico c'erano nella mano. Oppure il valore predefinito asso basso di uno doveva essere cambiato in 14 (valore più alto) e viceversa. In ogni caso il supporto del comportamento è stato delegato nuovamente alla classe Hand, poiché era un luogo molto naturale per tale comportamento. Di nuovo, era quasi come se un cervello umano fosse dietro la mano a fare questi calcoli.

La funzione di enumerazione dei vettori può essere utilizzata per scoprire quante carte di un valore specifico erano presenti nella 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

È molto importante scrivere codice di test ed esempi durante l'implementazione del framework effettivo. In questo modo saprai in ogni momento quanto bene funziona il codice di implementazione; ti rendi conto di fatti sulle caratteristiche e dettagli sull'implementazione. Con più tempo, avremmo implementato il poker: un simile test case avrebbe fornito una visione ancora più approfondita del problema e avrebbe mostrato come ridefinire il framework.