5장의 카드조합으로 높은패를 판단할수 있는것은 포커핸드 ( 투페어,풀하우드,스트레이트등) 라는 정의가 있어서 가능하며

Hand Rankings

각 핸드는 위와같이 순위가 있어서, 카드게임의 승패에 사용이된다.

이것을 OOP로 정의해보자, 자료구조는 앞장에서 만든 Card 5장을 받을수 있는 배열이 사용되었다.

PokerHand

고급적인 문법을 사용했다기 보다, IF문 신공을 펼쳤으며

저수준 IF문 사용에도 아래와같은 전략이 필요하다. 

  • 높은순에서 낮은순으로 검사한다.
  • 연속된 값 체크용이하게 하기위해 소팅을 한다.
  • 앞뒤 체크를 위해 이터레이터를 활용하였다.

Java8 Stream

JAVA 8 의 Stream을 일부 사용하였으며, for문 루프를 돌며 로컬 변수값을 통해 집계를 내는것보다

훨씬 짧고 유연한 코드 작성이 가능하다. 단점은 이러한 방식에 사용의 차이가 많이 있어서 언어를 변경하면

각각 배워야 하는것이고 절차적사고에서 선언형(함수형)방식의 사고방식 전환도 필요하다

동일한 기능을 가지고  C# Linq ↔ JAVA8 Stream  을 구현하는것은 더 좋은 학습예제가 될것이다.

SQL문 작성도 절차가 아닌, 선언형임을 알고 있자.

Java Stream이 얼마나 유용한지 살펴 보자..

다음 코드는 각 카드의 집계를 내는 코드이다.

5가 3장이고 6 이 2장이면 , 집계처리를 [5:3,6:2] 간편하게 해준다
    private Boolean isFullHouse(){
        Map<Integer, Integer> accumulator = new HashMap<>();
        hands.forEach(s -> accumulator.merge(s.getRank() , 1, Math::addExact));
        Boolean isFullHouse = (accumulator.containsValue(3) && accumulator.containsValue(2));
        return isFullHouse;
    }

풀하우스는 같은 점수의카드 3장 ,2장 조합인것으로 단지 위와같이 집계후 밸류값이 3,2두개가 있는지만 체크를 하면된다.

link : https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/


구현

package game.poker;

import java.util.*;

public class PokerHand implements Comparable<PokerHand>
{
    public enum HandRank{
        HIGHCARD, PAIR, TWOPAIR, THREEOFAKIND, STRAIGHT, FLUSH,
        FULLHOUSE, FOUROFAKIND, STRAIGHTFLUSH
    }

    static public int MAX_CARD =5;

    //private Card[] cards;
    private ArrayList<Card> hands;

    private ArrayList<Card> hitCards;

    //Stored Rank
    private HandRank handRank;
    private Card    highCard;
    private Card    lowCard;
    private Boolean isRunRank = false;

    public PokerHand(/*Card[] cards*/)
    {
        //this.setHand(cards);
        hitCards = new ArrayList<>();
    }

    public void setHand(Card[] cards)
    {
        if(cards.length != 5) throw new RuntimeException("Wrong number of cards. ");
        this.hands=new ArrayList<>(Arrays.asList(cards));
        isRunRank = false;
    }

    public void setHand(String handString){
        String[] cardsStrs = handString.split(" ");
        if(cardsStrs.length != 5) throw new RuntimeException("Wrong number of cards. ");

        this.hands=new ArrayList<>();
        //Arr Ver
        for (int i=0;i<cardsStrs.length;i++){
            Card addCard = new Card(cardsStrs[i]);
            hands.add(addCard);
        }
        isRunRank = false;
    }

    private boolean isFlush()
    {
        boolean resultValue = true;
        sortBySuit();
        Iterator<Card> cardIterator = hands.iterator();
        Card firstCard = cardIterator.next();
        while (cardIterator.hasNext()) {
            Card nextCard = cardIterator.next();
            if(firstCard.compareToSuit(nextCard)!=0){
                resultValue=false;
                break;
            }
        }
        if(resultValue){
            hitCards.add(hands.get(MAX_CARD-1));
            hitCards.add(hands.get(0));
        }
        return resultValue;
    }

    private boolean isStraight()
    {
        boolean resultValue = true;
        sortByRank();
        Iterator<Card> cardIterator = hands.iterator();
        Card preCard = null;
        Card firstCard = null;
        while (cardIterator.hasNext()) {
            Card curCard= cardIterator.next();
            if(preCard==null) firstCard = curCard;
            if(preCard!=null ){
                if( preCard.getRank()+1 != curCard.getRank() ){
                    resultValue = false;
                    break;
                }
            }
            preCard = curCard;
        }
        if(resultValue){
            hitCards.add(hands.get(MAX_CARD-1));
            hitCards.add(hands.get(0));
        }
        return resultValue;
    }

    private Boolean isFullHouse(){
        Map<Integer, Integer> accumulator = new HashMap<>();
        hands.forEach(s -> accumulator.merge(s.getRank() , 1, Math::addExact));
        Boolean isFullHouse = (accumulator.containsValue(3) && accumulator.containsValue(2));
        if(isFullHouse){
            hitCards.add(hands.get(MAX_CARD-1));
            hitCards.add(hands.get(0));
        }
        return isFullHouse;
    }

    private Boolean isExistSameKindRank(int chkCnt){
        Map<Integer, Integer> accumulator = new HashMap<>();
        hands.forEach(s -> accumulator.merge(s.getRank() , 1, Math::addExact ));
        Boolean isExist = false;
        for (Map.Entry<Integer, Integer> entry : accumulator.entrySet())
        {
            if(entry.getValue()==chkCnt){
                hitCards.add( new Card(entry.getKey(), Card.Suit.DUMMY) );
                isExist=true;
            }
        }
        return isExist;
    }

    private int pairCount(){
        Map<Integer, Integer> accumulator = new HashMap<>();
        hands.forEach(s -> accumulator.merge(s.getRank() , 1, Math::addExact ));
        int maxRank=0;
        int pairCount = 0;
        for (Map.Entry<Integer, Integer> entry : accumulator.entrySet())
        {
            if(entry.getValue()==2){
                pairCount++;
                maxRank=Math.max(maxRank,entry.getKey());
            }
        }
        //Integer maxPair = Collections.max(accumulator.entrySet(), Map.Entry.comparingByValue()).getValue();
        //Integer maxRank = Collections.max(accumulator.entrySet(), Map.Entry.comparingByValue()).getKey();
        if(pairCount > 0){
            hitCards.add( new Card(maxRank, Card.Suit.DUMMY) );
        }
        return pairCount;
    }

    private void sortBySuit()
    {
        Collections.sort(hands, (a, b) -> a.compareToSuit(b));
    }

    private void sortByRank()
    {
        Collections.sort(hands, (a, b) -> a.compareTo(b));
    }

    private void updateHighLowCard(){
        if(hitCards.size()>0){
            Collections.sort(hitCards, (a, b) -> a.compareTo(b));
            highCard = hitCards.get( hitCards.size()-1 );
            lowCard = hitCards.get(0);
        }else{
            sortByRank();
            highCard = hands.get(4);
            lowCard = hands.get(0);
        }
    }

    public HandRank getHandRank(){
        if(isRunRank==false){
            HandRank handRank = HandRank.HIGHCARD;
            int pairCount = pairCount();

            if(isStraight() && isFlush()){
                handRank = HandRank.STRAIGHTFLUSH;
            }else if(isExistSameKindRank(4) ){
                handRank = HandRank.FOUROFAKIND;
            }else if(isFullHouse()){
                handRank = HandRank.FULLHOUSE;
            }else if(isFlush()){
                handRank = HandRank.FLUSH;
            }else if(isStraight()){
                handRank = HandRank.STRAIGHT;
            }else if(isExistSameKindRank(3)){
                handRank = HandRank.THREEOFAKIND;
            }else if(pairCount()==2){
                handRank = HandRank.TWOPAIR;
            }else if(pairCount()==1){
                handRank = HandRank.PAIR;
            }
            this.handRank = handRank;
            updateHighLowCard();
        }
        return this.handRank;
    }

    public String toString()
    {
        String resultStr = this.handRank.toString() + " " + this.highCard.getRank();
        return resultStr;
    }

    public void printHand()
    {
        hands.forEach( card -> {
            System.out.print(card.getSuit() + ":" + card.getRank() + ", ");
        });
    }

    public int getValue(){
        if(isRunRank==false) getHandRank();
        return highCard.getValue() + handRank.ordinal()*100;
    }

    @Override
    public int compareTo(PokerHand other) {
        if (this.getValue()<other.getValue()) return -1;
        else if (this.getValue()>other.getValue()) return 1;
        else return 0;
    }

    @Override
    public boolean equals(Object o){
        PokerHand other = (PokerHand) o;
        return this.getValue() == other.getValue();
    }

    @Override
    public int hashCode(){
        assert false: "no hashcode implementation";
        return 17;
    }
}



HandTest by JUNIT

우리가 작성한 코드가 항상 의도대로 작동하지 않는다란 사실을 빨리 깨닫고 , 스스로 셀프 검증할수 있는

방법을 찾아야할것이며 JUNIT은 충분한 기능을 제공해준다. 습관화가 중요하다. 

@Test
public void handTest(){
    String[] testCase = {"2S 3S 4S 5S 6H", "2S 2D 2H 5S 6H", "2S 2D 2H 3S 3H","2S 2H 4H 5S 6H"};
    for(int idx=0 ; idx<testCase.length;idx++){
        System.out.println("### NewCard:");
        PokerHand hand = new PokerHand();
        hand.setHand(testCase[idx]);
        hand.getHandRank();
        System.out.println("PrintHand:");
        hand.printHand();
        System.out.println("HandStr:");
        System.out.println(hand);
    }
}

### NewCard:
PrintHand:
SPADES:2, SPADES:3, SPADES:4, SPADES:5, HEARTS:6, HandStr:
STRAIGHT 6
### NewCard:
PrintHand:
DIAMONDS:2, HEARTS:2, SPADES:2, SPADES:5, HEARTS:6, HandStr:
THREEOFAKIND 2
### NewCard:
PrintHand:
SPADES:2, DIAMONDS:2, HEARTS:2, SPADES:3, HEARTS:3, HandStr:
FULLHOUSE 3
### NewCard:
PrintHand:
HEARTS:2, SPADES:2, HEARTS:4, SPADES:5, HEARTS:6, HandStr:
PAIR 2

위 샘플은 수행결과를 눈으로 확인하기 위한 전통적인 방법이며(오래된)

유닛테스트의 본질은  assertThat,asserEqual 등으로 수행결과의 값과 예상한 값이 맞는지

코드가 변경될때마다 항상 수행해줘야하며 , 보통 빌드 배포시 자동화과정에 포함된다.

.

개발자 A에의해 핸드족보를 잘 판단하는 1차버전을 잘 만들었다.

하지만 운영중 족보계산은 성능이 너무 느렸고 성능개선을위해, 개발자 B가 Strem(Linq)를 if/if else 로 바꾸기로 결정하였다.

유닛테스트가 잘 작성된 경우 : 개발자 B는 코드레벨에서 성능 개선을 활동함과 동시에, 유닛테스트가 수시로 수행하여 자신의 로직이 잘 컨버트됨을 로컬에서 가능하다.

유닛테스트가 없는경우 : 개발자 B는 자신의 로직이  성능 개선됨을 검증하였지만, 족보계산이 잘되나 확인하기위해 수백판의 게임을 해야했다. ( 개발보다 테스트가 더 많은 시간이 소요됨)

참고 : 포커의 최고의 패를 보기위해서 수천만판을 해야할지도 모른다.     ( Fake Test가 없다고 하면 )






  • No labels
Write a comment…