Javaのカードエンジン

これはすべて、Javaで記述されたカードゲームアプリケーションまたはアプレットがほとんどないことに気付いたときに始まりました。まず、いくつかのゲームを書くことを考え、カードゲームの作成に必要なコアコードとクラスを理解することから始めました。プロセスは続行されますが、さまざまなカードゲームソリューションを作成するために使用できるかなり安定したフレームワークがあります。ここでは、このフレームワークがどのように設計されたか、どのように動作するか、そしてそれを有用で安定させるために使用されたツールとトリックについて説明します。

設計段階

オブジェクト指向設計では、問題の内外を知ることが非常に重要です。そうしないと、必要のない、または特定のニーズに従って機能しないクラスやソリューションの設計に多くの時間を費やす可能性があります。カードゲームの場合、1つのアプローチは、1人、2人、またはそれ以上の人がカードをプレイしたときに何が起こっているかを視覚化することです。

カードデッキには通常、4つの異なるスーツ(ダイヤ、ハート、クラブ、スペード)の52枚のカードが含まれ、値はデュースからキング、さらにエースまでさまざまです。すぐに問題が発生します。ゲームのルールに応じて、エースはカードの値が最も低いか、最も高いか、またはその両方になります。

さらに、デッキからカードを手札に取り、ルールに基づいて手札を管理するプレイヤーもいます。カードをテーブルに置いて全員に見せるか、個人的に見ることができます。ゲームの特定の段階によっては、手札にN枚のカードがある場合があります。

このようにステージを分析すると、さまざまなパターンが明らかになります。ここでは、上記のように、IvarJacobsonのオブジェクト指向ソフトウェアエンジニアリングに記載されているケースドリブンアプローチを使用します。この本では、基本的な考え方の1つは、実際の状況に基づいてクラスをモデル化することです。これにより、関係がどのように機能するか、何が何に依存するか、抽象化がどのように機能するかを理解しやすくなります。

CardDeck、Hand、Card、RuleSetなどのクラスがあります。CardDeckには最初に52個のCardオブジェクトが含まれ、CardDeckにはHandオブジェクトに描画されるため、Cardオブジェクトの数が少なくなります。ハンドオブジェクトは、ゲームに関するすべてのルールを持つRuleSetオブジェクトと通信します。RuleSetをゲームハンドブックと考えてください。

ベクトルクラス

この場合、動的なエントリの変更を処理する柔軟なデータ構造が必要でした。これにより、配列データ構造が排除されました。また、挿入要素を追加し、可能であれば多くのコーディングを回避する簡単な方法も必要でした。さまざまな形式の二分木など、さまざまなソリューションを利用できます。ただし、java.utilパッケージにはVectorクラスがあり、必要に応じてサイズが拡大および縮小するオブジェクトの配列を実装します。これはまさに私たちが必要としていたものです。 (Vectorメンバー関数は、現在のドキュメントでは完全には説明されていません。この記事では、Vectorクラスを同様の動的オブジェクトリストインスタンスに使用する方法についてさらに説明します。)Vectorクラスの欠点は、メモリが多いためにメモリを追加で使用することです。コピーは舞台裏で行われます。 (このため、配列は常に優れています。サイズは静的ですが、そのため、コンパイラはコードを最適化する方法を見つけることができます)。また、オブジェクトのセットが大きいと、ルックアップ時間に関してペナルティが発生する可能性がありますが、考えられる最大のベクトルは52エントリでした。この場合でもそれは合理的であり、長いルックアップ時間は問題ではありませんでした。

各クラスがどのように設計および実装されたかについて、簡単に説明します。

カードクラス

Cardクラスは非常に単純なもので、色と値を示す値が含まれています。また、アニメーション(カードを裏返す)などの考えられる単純な動作を含む、カードを説明するGIF画像や同様のエンティティへのポインタが含まれている場合もあります。

クラスCardはCardConstantsを実装します{publicint color; public int value; public String ImageName; }

これらのCardオブジェクトは、さまざまなVectorクラスに格納されます。色を含むカードの値はインターフェイスで定義されていることに注意してください。これは、フレームワークの各クラスが実装でき、この方法で定数が含まれることを意味します。

interface CardConstants {//インターフェイスフィールドは常にpublicstatic finalです!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; }

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 HEARTS; aCard.value i; Deck.addElement(aCard); } // CLUBS、DIAMONDS、SPADESについても同じようにします。}

CardDeckからCardオブジェクトを描画するときは、ベクトル内のランダムな位置を選択するセットを知っている乱数ジェネレーターを使用しています。つまり、Cardオブジェクトが順序付けられている場合でも、ランダム関数はVector内の要素のスコープ内の任意の位置を選択します。

このプロセスの一環として、このオブジェクトをHandクラスに渡すときに、CardDeckベクトルから実際のオブジェクトも削除します。Vectorクラスは、カードを渡すことによって、カードデッキと手の実際の状況をマップします。

public Card draw(){Card aCard null; int位置(int)(Math.random()*(deck.size =())); {aCard(カード)deck.elementAt(位置);を試してください。} catch(ArrayIndexOutOfBoundsException e){e.printStackTrace(); } Deck.removeElementAt(位置); カードを返す; }

存在しない位置からベクターからオブジェクトを取得することに関連する可能性のある例外をキャッチすることは良いことに注意してください。

ベクトル内のすべての要素を反復処理し、ASCII値/色のペア文字列をダンプする別のメソッドを呼び出すユーティリティメソッドがあります。この機能は、DeckクラスとHandクラスの両方をデバッグするときに役立ちます。ベクトルの列挙機能は、Handクラスでよく使用されます。

public void dump(){列挙型enum Deck.elements(); while(enum.hasMoreElements()){カードカード(カード)enum.nextElement(); RuleSet.printValue(カード); }}

ハンドクラス

Handクラスは、このフレームワークの真の主力製品です。必要な行動のほとんどは、このクラスに入れるのが非常に自然なことでした。カードを手に持って、カードのオブジェクトを見ながらさまざまな操作をしている人を想像してみてください。

まず、多くの場合、何枚のカードがピックアップされるかが不明であるため、ベクトルも必要です。配列を実装することもできますが、ここでもある程度の柔軟性があると便利です。私たちが必要とする最も自然な方法は、カードを取ることです:

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

CardHandはベクトルなので、Cardオブジェクトをこのベクトルに追加するだけです。ただし、手札からの「出力」操作の場合、カードを提示する場合と、手札からカードを提示して引く場合の2つの場合があります。両方を実装する必要がありますが、カードの描画と表示はカードを表示するだけの特殊なケースであるため、継承を使用するとコードの記述が少なくなります。

public Card show(int position){Card aCard null; {aCard(Card)cardHand.elementAt(position);を試してください。} catch(ArrayIndexOutOfBoundsException e){e.printStackTrace(); } return aCard; } 20パブリックカードドロー(int位置){カードaCardショー(位置); cardHand.removeElementAt(位置); カードを返す; }

言い換えると、ドローケースはショーケースであり、Handベクトルからオブジェクトを削除するという追加の動作があります。

さまざまなクラスのテストコードを作成する際に、手にあるさまざまな特別な値を見つける必要があるケースが増えていることがわかりました。たとえば、特定の種類のカードが何枚手札にあるかを知る必要がある場合がありました。または、デフォルトのエースの低い値である1を14(最高値)に変更して、元に戻す必要がありました。いずれの場合も、そのような行動にとって非常に自然な場所であったため、行動サポートはHandクラスに委任されました。繰り返しますが、それはまるで人間の脳がこれらの計算をしている手の後ろにいるかのようでした。

ベクトルの列挙機能を使用して、Handクラスに存在する特定の値のカードの数を調べることができます。

public int NCards(int value){int n 0; 列挙型列挙型cardHand.elements(); while(enum.hasMoreElements()){tempCard(Card)enum.nextElement(); // = tempCardが定義されているif(tempCard.value = value)n ++; } return n; }

同様に、カードオブジェクトを反復処理して、カードの合計を計算したり(21テストのように)、カードの値を変更したりできます。デフォルトでは、すべてのオブジェクトはJavaの参照であることに注意してください。一時オブジェクトと思われるものを取得して変更すると、ベクトルによって格納されているオブジェクト内の実際の値も変更されます。これは覚えておくべき重要な問題です。

RuleSetクラス

RuleSetクラスは、ゲームをプレイするときに時々チェックするルールブックのようなものです。ルールに関するすべての動作が含まれています。ゲームプレーヤーが使用できる戦略は、ユーザーインターフェイスのフィードバック、または単純またはより複雑な人工知能(AI)コードのいずれかに基づいていることに注意してください。RuleSetが心配しているのは、ルールが守られていることだけです。

カードに関連する他の行動もこのクラスに入れられました。たとえば、カードの値情報を出力する静的関数を作成しました。後で、これを静的関数としてCardクラスに配置することもできます。現在の形式では、RuleSetクラスには基本的なルールが1つだけあります。2枚のカードを受け取り、どちらのカードが最も高いかに関する情報を送り返します。

 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

実際のフレームワークを実装しながら、テストコードと例を作成することは非常に重要です。このようにして、実装コードがどれだけうまく機能するかを常に知ることができます。機能に関する事実と実装に関する詳細を理解します。もっと時間があれば、ポーカーを実装できたでしょう。そのようなテストケースは、問題についてさらに多くの洞察を提供し、フレームワークを再定義する方法を示していたでしょう。