자바의 카드 엔진

이 모든 것은 Java로 작성된 카드 게임 응용 프로그램이나 애플릿이 거의 없다는 것을 알았을 때 시작되었습니다. 먼저 우리는 몇 가지 게임을 작성하는 것에 대해 생각하고 카드 게임을 만드는 데 필요한 핵심 코드와 클래스를 파악하는 것으로 시작했습니다. 프로세스는 계속되지만 이제 다양한 카드 게임 솔루션을 만드는 데 사용할 수있는 상당히 안정적인 프레임 워크가 있습니다. 여기에서는이 프레임 워크가 어떻게 설계되었는지, 어떻게 작동하는지, 유용하고 안정적으로 만드는 데 사용 된 도구와 트릭에 대해 설명합니다.

디자인 단계

객체 지향 디자인에서는 문제를 안팎으로 아는 것이 매우 중요합니다. 그렇지 않으면 필요하지 않거나 특정 요구에 따라 작동하지 않는 클래스와 솔루션을 디자인하는 데 많은 시간을 할애 할 수 있습니다. 카드 게임의 경우 한 가지 접근 방식은 한 명, 두 명 또는 그 이상의 사람이 카드를 할 때 무슨 일이 벌어지는 지 시각화하는 것입니다.

카드 덱에는 일반적으로 듀스에서 킹까지의 값과 에이스에 이르기까지 4 가지 종류 (다이아몬드, 하트, 클럽, 스페이드)의 52 장의 카드가 포함됩니다. 즉시 문제가 발생합니다. 게임 규칙에 따라 에이스는 가장 낮은 카드 값, 가장 높은 값 또는 둘 다가 될 수 있습니다.

또한 덱에서 카드를 손으로 가져와 규칙에 따라 손을 관리하는 플레이어가 있습니다. 카드를 테이블에 올려 놓거나 개인적으로 볼 수 있습니다. 게임의 특정 단계에 따라 손에 N 개의 카드가있을 수 있습니다.

이러한 방식으로 단계를 분석하면 다양한 패턴이 드러납니다. 이제 위에서 설명한대로 Ivar Jacobson의 객체 지향 소프트웨어 엔지니어링에 문서화 된 사례 중심 접근 방식을 사용합니다 . 이 책에서 기본 아이디어 중 하나는 실제 상황을 기반으로 클래스를 모델링하는 것입니다. 이를 통해 관계가 작동하는 방식, 무엇이 좌우되는지, 추상화가 작동하는 방식을 훨씬 쉽게 이해할 수 있습니다.

CardDeck, Hand, Card 및 RuleSet과 같은 클래스가 있습니다. CardDeck은 처음에 52 개의 Card 개체를 포함하고 CardDeck은 Hand 개체에 그려 지므로 Card 개체가 더 적습니다. 손 객체는 게임과 관련된 모든 규칙을 가진 RuleSet 객체와 대화합니다. RuleSet을 게임 핸드북으로 생각하십시오.

벡터 클래스

이 경우 동적 항목 변경을 처리하는 유연한 데이터 구조가 필요하여 Array 데이터 구조가 제거되었습니다. 또한 삽입 요소를 쉽게 추가하고 가능하면 많은 코딩을 피하는 방법을 원했습니다. 다양한 형태의 이진 트리와 같은 다양한 솔루션을 사용할 수 있습니다. 그러나 java.util 패키지에는 필요에 따라 크기가 늘어나고 줄어드는 객체 배열을 구현하는 Vector 클래스가 있습니다. (Vector 멤버 함수는 현재 문서에 완전히 설명되어 있지 않습니다.이 기사에서는 Vector 클래스를 유사한 동적 객체 목록 인스턴스에 사용하는 방법을 자세히 설명합니다.) Vector 클래스의 단점은 많은 메모리로 인한 추가 메모리 사용입니다. 이면에서 복사가 이루어집니다. (이러한 이유로 배열은 항상 더 좋습니다. 크기가 정적이고그래서 컴파일러는 코드를 최적화하는 방법을 알아낼 수 있습니다). 또한 더 큰 객체 세트를 사용하면 조회 시간에 대한 불이익이있을 수 있지만 생각할 수있는 가장 큰 벡터는 52 개 항목이었습니다. 이 경우에도 여전히 합리적이며 긴 조회 시간은 문제가되지 않았습니다.

다음은 각 클래스가 어떻게 설계되고 구현되었는지에 대한 간략한 설명입니다.

카드 클래스

Card 클래스는 매우 간단합니다. 여기에는 색상과 값을 나타내는 값이 포함되어 있습니다. 또한 애니메이션 (카드 뒤집기) 등과 같은 가능한 간단한 동작을 포함하여 카드를 설명하는 GIF 이미지 및 유사한 엔티티에 대한 포인터가있을 수 있습니다.

class Card는 CardConstants {public int color; public int 값; public String ImageName; }

이러한 Card 객체는 다양한 Vector 클래스에 저장됩니다. 색상을 포함한 카드의 값은 인터페이스에 정의되어 있습니다. 즉, 프레임 워크의 각 클래스가 구현할 수 있으며이 방식으로 상수가 포함됩니다.

interface CardConstants {// 인터페이스 필드는 항상 public static final입니다! int HEARTS 1; int 다이아몬드 2; int SPADE 3; int CLUBS 4; int JACK 11; int 퀸 12; int KING 13; int ACE_LOW 1; int ACE_HIGH 14; }

CardDeck 클래스

CardDeck 클래스에는 52 개의 카드 개체로 사전 초기화되는 내부 벡터 개체가 있습니다. 이것은 shuffle이라는 방법을 사용하여 수행됩니다. 의미는 섞을 때마다 52 개의 카드를 정의하여 게임을 시작한다는 것입니다. 가능한 모든 이전 개체를 제거하고 기본 상태에서 다시 시작해야합니다 (52 개의 카드 개체).

public void shuffle () {// 항상 데크 벡터를 0으로 만들고 처음부터 초기화합니다. 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 객체가 정렬 된 경우에도 random 함수는 Vector 내부의 요소 범위 내에서 임의의 위치를 ​​선택합니다.

이 프로세스의 일부로이 객체를 Hand 클래스에 전달할 때 CardDeck 벡터에서 실제 객체도 제거합니다. Vector 클래스는 카드를 전달하여 카드 데크와 핸드의 실제 상황을 매핑합니다.

public Card draw () {Card aCard null; int 위치 (int) (Math.random () * (deck.size = ())); try {aCard (카드) deck.elementAt (위치); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } deck.removeElementAt (위치); return aCard; }

존재하지 않는 위치에서 Vector에서 객체를 가져 오는 것과 관련된 가능한 예외를 포착하는 것이 좋습니다.

벡터의 모든 요소를 ​​반복하고 ASCII 값 / 색상 쌍 문자열을 덤프하는 다른 메서드를 호출하는 유틸리티 메서드가 있습니다. 이 기능은 Deck 및 Hand 클래스를 모두 디버깅 할 때 유용합니다. 벡터의 열거 기능은 Hand 클래스에서 많이 사용됩니다.

public void dump () {열거 열거 deck.elements (); while (enum.hasMoreElements ()) {카드 카드 (카드) enum.nextElement (); RuleSet.printValue (카드); }}

핸드 클래스

Hand 클래스는이 프레임 워크에서 진정한 도구입니다. 필요한 행동의 대부분은이 수업에 배치하기에 매우 자연스러운 것이었다. 사람들이 손에 카드를 들고 카드 개체를 보면서 다양한 작업을한다고 상상해보십시오.

첫째, 벡터도 필요합니다. 많은 경우에 얼마나 많은 카드를 집을 지 알 수 없기 때문입니다. 배열을 구현할 수 있지만 여기서도 약간의 유연성을 갖는 것이 좋습니다. 가장 자연스러운 방법은 카드를 가져 오는 것입니다.

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

CardHand벡터이므로이 벡터에 Card 객체를 추가합니다. 그러나 손에서 "출력"연산의 경우 두 가지 경우가 있습니다. 하나는 카드를 보여주고 다른 하나는 손에서 카드를 보여주고 뽑습니다. 두 가지를 모두 구현해야하지만 상속을 사용하면 코드를 덜 작성합니다. 카드를 그리고 표시하는 것은 카드를 표시하는 것의 특별한 경우이기 때문입니다.

public Card show (int position) {Card aCard null; try {aCard (카드) cardHand.elementAt (위치); } catch (ArrayIndexOutOfBoundsException e) {e.printStackTrace (); } return aCard; } 20 공개 카드 뽑기 (int position) {Card aCard show (position); cardHand.removeElementAt (위치); return aCard; }

즉, 그리기 케이스는 Hand 벡터에서 객체를 제거하는 추가 동작이있는 쇼 케이스입니다.

다양한 클래스에 대한 테스트 코드를 작성하는 과정에서 다양한 특수 값에 대해 알아 내야하는 경우가 증가하고 있음을 발견했습니다. 예를 들어, 때때로 우리는 특정 유형의 카드가 몇 장인지 알아야했습니다. 또는 1의 기본 ace low 값을 14 (가장 높은 값)로 변경하고 다시 되돌려 야했습니다. 모든 경우에 행동 지원은 Hand 클래스로 다시 위임되었습니다. 그러한 행동을위한 매우 자연스러운 장소 였기 때문입니다. 다시 말하지만, 마치 인간의 두뇌가 이러한 계산을 수행하는 것과 거의 같습니다.

벡터의 열거 기능을 사용하여 특정 값의 카드가 Hand 클래스에 몇 개 있는지 확인할 수 있습니다.

public int NCards (int 값) {int n 0; 열거 형 enum cardHand.elements (); while (enum.hasMoreElements ()) {tempCard (카드) enum.nextElement (); // = tempCard 정의 if (tempCard.value = value) n ++; } return n; }

마찬가지로 카드 개체를 반복하고 카드의 총합을 계산하거나 (21 번 테스트에서와 같이) 카드의 값을 변경할 수 있습니다. 기본적으로 모든 개체는 Java의 참조입니다. 임시 객체라고 생각되는 것을 검색하여 수정하면 실제 값도 벡터에 저장된 객체 내에서 변경됩니다. 이것은 명심해야 할 중요한 문제입니다.

RuleSet 클래스

RuleSet 클래스는 게임을 할 때마다 확인하는 규칙 책과 같습니다. 규칙과 관련된 모든 동작을 포함합니다. 게임 플레이어가 사용할 수있는 가능한 전략은 사용자 인터페이스 피드백이나 단순하거나 더 복잡한 인공 지능 (AI) 코드를 기반으로합니다. 모든 RuleSet이 걱정하는 것은 규칙이 준수된다는 것입니다.

카드와 관련된 다른 행동도이 클래스에 배치되었습니다. 예를 들어 카드 값 정보를 인쇄하는 정적 함수를 만들었습니다. 나중에 이것은 정적 함수로 Card 클래스에 배치 될 수도 있습니다. 현재 양식에서 RuleSet 클래스에는 하나의 기본 규칙 만 있습니다. 두 장의 카드를 사용하여 가장 높은 카드에 대한 정보를 다시 보냅니다.

public int 더 높은 (카드 1, 카드 2) {int whichone 0; if (one.value = ACE_LOW) one.value ACE_HIGH; if (two.value = ACE_LOW) two.value ACE_HIGH; //이 규칙 세트에서 가장 높은 값이 승리합니다. // 색상은 고려하지 않습니다. if (one.value> two.value) whichone 1; if (one.value <two.value) whichone 2; if (one.value = two.value) whichone 0; // 전달 된 값이 동일한 값을 갖도록 ACE 값을 정규화합니다. if (one.value = ACE_HIGH) one.value ACE_LOW; if (two.value = ACE_HIGH) two.value ACE_LOW; 어느 것을 반환하십시오; }

테스트를 수행하는 동안 자연 값이 1에서 14 인 에이스 값을 변경해야합니다. 이 프레임 워크에서 에이스는 항상 하나라고 가정하므로 가능한 문제를 피하기 위해 나중에 값을 다시 1로 변경하는 것이 중요합니다.

21의 경우, 우리는 손이 21 미만인지, 정확히 21인지 또는 21 이상인지 알아내는 방법을 아는 TwentyOneRuleSet 클래스를 만들기 위해 RuleSet을 서브 클래 싱했습니다. 또한 1 또는 14가 될 수있는 에이스 값을 고려합니다. 가능한 최상의 가치를 알아 내려고합니다. (더 많은 예를 보려면 소스 코드를 참조하십시오.) 그러나 전략을 정의하는 것은 플레이어의 몫입니다. 이 경우, 우리는 두 장의 카드 후에 자신의 핸드가 21 미만이면 카드 한 장을 더 가져 가서 멈추는 단순한 AI 시스템을 작성했습니다.

수업 사용 방법

이 프레임 워크를 사용하는 것은 매우 간단합니다.

myCardDeck 새로운 CardDeck (); myRules new RuleSet (); handA 새로운 손 (); handB 새로운 손 (); DebugClass.DebugStr ( "A와 B를 각각 5 장 뽑습니다."); for (int i 0; i <NCARDS; i ++) {handA.take (myCardDeck.draw ()); handB.take (myCardDeck.draw ()); } // 프로그램을 테스트하고, 주석 처리하거나 DEBUG 플래그를 사용하여 비활성화합니다. testHandValues ​​(); testCardDeckOperations (); testCardValues ​​(); testHighestCardValues ​​(); test21 ();

다양한 테스트 프로그램은 별도의 정적 또는 비 정적 멤버 함수로 분리됩니다. 원하는만큼의 손을 만들고, 카드를 가져오고, 가비지 컬렉션에서 사용하지 않는 손과 카드를 제거하도록합니다.

손 또는 카드 개체를 제공하여 RuleSet을 호출하고 반환 된 값을 기반으로 결과를 알 수 있습니다.

 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

실제 프레임 워크를 구현하는 동안 테스트 코드와 예제를 작성하는 것은 매우 중요합니다. 이렇게하면 구현 코드가 얼마나 잘 작동하는지 항상 알 수 있습니다. 기능에 대한 사실과 구현에 대한 세부 정보를 알게됩니다. 더 많은 시간이 주어지면 포커를 구현했을 것입니다. 이러한 테스트 사례는 문제에 대한 더 많은 통찰력을 제공하고 프레임 워크를 재정의하는 방법을 보여줄 것입니다.