Motor de tarjetas en Java

Todo comenzó cuando nos dimos cuenta de que había muy pocas aplicaciones de juegos de cartas o subprogramas escritos en Java. Primero pensamos en escribir un par de juegos y comenzamos por descubrir el código central y las clases necesarias para crear juegos de cartas. El proceso continúa, pero ahora hay un marco bastante estable para usar para crear varias soluciones de juegos de cartas. Aquí describimos cómo se diseñó este marco, cómo funciona y las herramientas y trucos que se utilizaron para hacerlo útil y estable.

Fase de diseño

Con el diseño orientado a objetos, es extremadamente importante conocer el problema por dentro y por fuera. De lo contrario, es posible dedicar mucho tiempo a diseñar clases y soluciones que no son necesarias o que no funcionarán de acuerdo con necesidades específicas. En el caso de los juegos de cartas, un enfoque es visualizar lo que sucede cuando una, dos o más personas juegan a las cartas.

Una baraja de cartas generalmente contiene 52 cartas de cuatro palos diferentes (diamantes, corazones, tréboles, espadas), con valores que van del dos al rey, más el as. Inmediatamente surge un problema: dependiendo de las reglas del juego, los ases pueden ser el valor de carta más bajo, el más alto o ambos.

Además, hay jugadores que toman cartas del mazo en una mano y administran la mano según las reglas. Puede mostrar las cartas a todos colocándolas sobre la mesa o mirarlas en privado. Dependiendo de la etapa particular del juego, es posible que tenga N número de cartas en su mano.

Analizar las etapas de esta manera revela varios patrones. Ahora usamos un enfoque basado en casos, como se describió anteriormente, que está documentado en Ingeniería de software orientada a objetos de Ivar Jacobson . En este libro, una de las ideas básicas es modelar clases basadas en situaciones de la vida real. Eso hace que sea mucho más fácil comprender cómo operan las relaciones, qué depende de qué y cómo operan las abstracciones.

Tenemos clases como CardDeck, Hand, Card y RuleSet. Un CardDeck contendrá 52 objetos de Cartas al principio, y CardDeck tendrá menos objetos de Cartas ya que estos se dibujan en un objeto de Mano. Los objetos de mano hablan con un objeto RuleSet que tiene todas las reglas relativas al juego. Piense en un RuleSet como el manual del juego.

Clases de vectores

En este caso, necesitábamos una estructura de datos flexible que manejara cambios de entrada dinámicos, lo que eliminó la estructura de datos Array. También queríamos una forma fácil de agregar un elemento de inserción y evitar mucha codificación si es posible. Hay diferentes soluciones disponibles, como varias formas de árboles binarios. Sin embargo, el paquete java.util tiene una clase Vector que implementa una matriz de objetos que crece y se reduce de tamaño según sea necesario, que era exactamente lo que necesitábamos. (Las funciones de los miembros de Vector no se explican completamente en la documentación actual; este artículo explicará con más detalle cómo se puede usar la clase Vector para instancias de listas de objetos dinámicos similares). El inconveniente de las clases Vector es el uso de memoria adicional, debido a una gran cantidad de memoria copia hecha detrás de escena. (Por esta razón, las matrices siempre son mejores; son de tamaño estático,para que el compilador pueda encontrar formas de optimizar el código). Además, con conjuntos de objetos más grandes, podríamos tener penalizaciones con respecto a los tiempos de búsqueda, pero el Vector más grande que pudimos pensar fue 52 entradas. Eso sigue siendo razonable para este caso, y los tiempos de búsqueda prolongados no eran una preocupación.

A continuación, se ofrece una breve explicación de cómo se diseñó e implementó cada clase.

Clase de tarjeta

La clase Card es muy simple: contiene valores que indican el color y el valor. También puede tener indicadores de imágenes GIF y entidades similares que describen la tarjeta, incluido un posible comportamiento simple como una animación (voltear una tarjeta), etc.

class Card implementa CardConstants {public int color; valor int público; public String ImageName; }

Estos objetos Card se almacenan en varias clases de Vector. Tenga en cuenta que los valores de las tarjetas, incluido el color, se definen en una interfaz, lo que significa que cada clase en el marco podría implementar y de esta manera incluir las constantes:

interface CardConstants {// ¡los campos de la interfaz son siempre públicos estáticos finales! int CORAZONES 1; int DIAMANTE 2; int SPADE 3; int CLUBS 4; int JACK 11; int QUEEN 12; int REY 13; int ACE_LOW 1; int ACE_HIGH 14; }

Clase CardDeck

La clase CardDeck tendrá un objeto Vector interno, que estará preinicializado con 52 objetos de tarjeta. Esto se hace mediante un método llamado shuffle. La implicación es que cada vez que barajas, empiezas un juego definiendo 52 cartas. Es necesario eliminar todos los objetos antiguos posibles y comenzar desde el estado predeterminado nuevamente (52 objetos de tarjeta).

public void shuffle () {// Siempre ponga a cero el vector de cubierta e inicialícelo desde cero. deck.removeAllElements (); 20 // Luego inserta las 52 cartas. Un color a la vez para (int i ACE_LOW; i <ACE_HIGH; i ++) {Tarjeta aCard nueva Tarjeta (); aCard.color CORAZONES; aCard.value i; deck.addElement (aCard); } // Haz lo mismo con CLUBS, DIAMONDS y SPADES. }

Cuando sacamos un objeto Card del CardDeck, estamos usando un generador de números aleatorios que conoce el conjunto del que elegirá una posición aleatoria dentro del vector. En otras palabras, incluso si los objetos Card están ordenados, la función aleatoria elegirá una posición arbitraria dentro del alcance de los elementos dentro del Vector.

Como parte de este proceso, también eliminamos el objeto real del vector CardDeck a medida que pasamos este objeto a la clase Hand. La clase Vector mapea la situación de la vida real de una baraja de cartas y una mano pasando una carta:

public Card draw () {Card aCard nulo; int position (int) (Math.random () * (deck.size = ())); intente {unaCard (Card) deck.elementAt (posición); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (posición); devolver una tarjeta; }

Tenga en cuenta que es bueno detectar cualquier posible excepción relacionada con la toma de un objeto del Vector desde una posición que no está presente.

Hay un método de utilidad que recorre en iteración todos los elementos del vector y llama a otro método que volcará una cadena de par de valores / colores ASCII. Esta función es útil cuando se depuran las clases Deck y Hand. Las características de enumeración de los vectores se utilizan mucho en la clase Hand:

public void dump () {Enumeración enum deck.elements (); while (enum.hasMoreElements ()) {Tarjeta tarjeta (Tarjeta) enum.nextElement (); RuleSet.printValue (tarjeta); }}

Clase de mano

La clase Hand es un verdadero caballo de batalla en este marco. La mayor parte del comportamiento requerido era algo muy natural de ubicar en esta clase. Imagínese personas sosteniendo tarjetas en sus manos y realizando diversas operaciones mientras miran los objetos de la tarjeta.

Primero, también necesita un vector, ya que en muchos casos se desconoce cuántas tarjetas se recogerán. Aunque podría implementar una matriz, también es bueno tener cierta flexibilidad aquí. El método más natural que necesitamos es tomar una tarjeta:

toma pública nula (Tarjeta theCard) {cardHand.addElement (theCard); }

CardHandes un vector, por lo que solo estamos agregando el objeto Card en este vector. Sin embargo, en el caso de las operaciones de "salida" de la mano, tenemos dos casos: uno en el que mostramos la carta y otro en el que ambos mostramos y robamos la carta de la mano. Necesitamos implementar ambos, pero usando la herencia escribimos menos código porque dibujar y mostrar una tarjeta es un caso especial de simplemente mostrar una tarjeta:

Mostrar tarjeta pública (posición int) {Tarjeta aCard nula; intente {aCard (Card) cardHand.elementAt (posición); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } return aCard; } 20 Robo de cartas públicas (posición int) {Mostrar carta aCard (posición); cardHand.removeElementAt (posición); devolver una tarjeta; }

En otras palabras, el caso de dibujo es un caso de demostración, con el comportamiento adicional de eliminar el objeto del vector Mano.

Al escribir el código de prueba para las diversas clases, encontramos un número creciente de casos en los que era necesario conocer varios valores especiales en la mano. Por ejemplo, a veces necesitábamos saber cuántas cartas de un tipo específico había en la mano. O el valor bajo predeterminado de uno tenía que cambiarse a 14 (valor más alto) y viceversa. En todos los casos, el apoyo conductual se delegó nuevamente en la clase Hand, ya que era un lugar muy natural para tal comportamiento. Una vez más, era casi como si un cerebro humano estuviera detrás de la mano haciendo estos cálculos.

La función de enumeración de vectores se puede utilizar para averiguar cuántas cartas de un valor específico estaban presentes en la clase Mano:

 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 muy importante escribir código de prueba y ejemplos mientras se implementa el marco real. De esta forma, sabrá en todo momento qué tan bien funciona el código de implementación; se da cuenta de los hechos sobre las características y los detalles sobre la implementación. Con más tiempo, habríamos implementado el póquer; un caso de prueba de este tipo habría proporcionado aún más información sobre el problema y habría mostrado cómo redefinir el marco.