What is it, naokirin?

テスト駆動開発の練習(17) -C++&GoogleTestでポーカーの役判定(3)-

とりあえず、今回で役判定の記事は最後にします。

今回は存在しないカードの数字やスートも数字も同じカードが2枚以上無いかと言った反則に当たるものがないかを判定しています。また、前回までのコードの問題点を直し、リファクタリングもしています。(結局テストコードのリファクタリングがされていませんが…)

では実際のコードです。

PokerTest.cpp

#include <gtest/gtest.h>
#include "Poker.h"

class PokerTest : public ::testing::Test {
public:
  Poker *poker;
  
protected:
  virtual void SetUp() {
    poker = new Poker();
  }
  
  virtual void TearDown() {
    delete poker;
  }
};

TEST_F(PokerTest, pokerHandsTest) {
  Card onePair[5] = {{HEART, 1}, {SPADE, 1}, {SPADE, 2}, {CLUB, 4}, {CLUB, 3}};
  EXPECT_EQ("one pair", poker->pokerHands(onePair));
  
  Card twoPair[5] = {{CLUB ,1}, {HEART ,2}, {DIAMOND ,3}, {HEART ,1}, {SPADE ,2}};
  EXPECT_EQ("two pair", poker->pokerHands(twoPair));
  
  Card fullHouse[5] = {{SPADE ,1}, {HEART ,2}, {CLUB ,1}, {DIAMOND, 1}, {DIAMOND ,2}};
  EXPECT_EQ("full house", poker->pokerHands(fullHouse));
  
  Card trips[5] = {{DIAMOND ,1}, {DIAMOND, 2}, {SPADE, 2}, {DIAMOND ,3}, {HEART ,2}};
  EXPECT_EQ("three of a kind", poker->pokerHands(trips));
  
  Card quads[5] = {{SPADE ,13}, {SPADE ,1}, {HEART ,1}, {DIAMOND ,1}, {CLUB ,1}};
  EXPECT_EQ("four of a kind", poker->pokerHands(quads));
  
  Card straight[5] = {{SPADE ,1}, {HEART ,2}, {CLUB ,3}, {DIAMOND ,4}, {SPADE ,5}};
  EXPECT_EQ("straight", poker->pokerHands(straight));
  
  Card straight2[5] = {{SPADE, 12}, {HEART, 11}, {SPADE, 13}, {HEART, 10}, {CLUB, 1}};
  EXPECT_EQ("straight", poker->pokerHands(straight2));
  
  Card flush[5] = {{HEART, 1}, {HEART, 4}, {HEART, 6}, {HEART, 9}, {HEART, 12}};
  EXPECT_EQ("flush", poker->pokerHands(flush));
  
  Card straightFlush[5] = {{HEART, 5}, {HEART, 6}, {HEART, 4}, {HEART, 2}, {HEART, 3}};
  EXPECT_EQ("straight flush", poker->pokerHands(straightFlush));
  
  Card loyalStraightFlush[5] = {{HEART, 12}, {HEART, 11}, {HEART, 13}, {HEART, 10}, {HEART, 1}};
  EXPECT_EQ("loyal straight flush", poker->pokerHands(loyalStraightFlush));
  
  Card boo[5] = {{DIAMOND, 1}, {DIAMOND, 3}, {HEART, 5}, {DIAMOND, 4}, {DIAMOND, 7}};
  EXPECT_EQ("boo", poker->pokerHands(boo));
}

TEST_F(PokerTest, isFoulTest){
  Card notFoul[5] = {{HEART, 1}, {SPADE, 1}, {SPADE, 2}, {CLUB, 4}, {CLUB, 3}};
  EXPECT_TRUE( poker->isFoul( notFoul ) );
  
  Card sameCard[5] = {{SPADE, 1}, {SPADE, 1}, {SPADE, 6}, {DIAMOND, 2}, {CLUB, 3}};
  EXPECT_FALSE( poker->isFoul( sameCard ) );

  Card over13[5] = {{SPADE, 1}, {DIAMOND, 1}, {SPADE, 6}, {DIAMOND, 14}, {CLUB, 3}};
  EXPECT_FALSE( poker->isFoul( over13 ) );
  
  Card under1[5] = {{SPADE, 1}, {DIAMOND, 0}, {SPADE, 6}, {DIAMOND, 13}, {CLUB, 3}};
  EXPECT_FALSE( poker->isFoul( under1 ) );
}

Poker.h & Poker.cpp

// Poker.h
#include <string>

enum Suit{
  SPADE,
  HEART,
  DIAMOND,
  CLUB
};

struct Card {
  Suit suit;
  int number;
  
  const bool operator ==( const Card card );
};

struct Poker {
  Poker();
  ~Poker();
  
  const std::string pokerHands( Card array[5] );
  const bool isFoul( Card hand[5] );

private:
  const int sameNumberCard( const int numArray[5] );
  const bool isStraight( const int sortedNumArray[5] );
  const bool isFlush( const Suit suitArray[5] );
  const bool isLoyal( const int sortedNumArray[5] );
};

// Poker.cpp
#include "Poker.h"
#include <algorithm>

Poker::Poker() {}

Poker::~Poker() {}

const std::string Poker::pokerHands( Card array[5] ) {
  
  int numArray[5];
  Suit suitArray[5];
  
  for( int i = 0; i < 5; ++i ){
    numArray[i] = array[i].number;
    suitArray[i] = array[i].suit;
  }
  
  int sameNumberCard = this->sameNumberCard( numArray );

  if ( !(this->isFoul( array ))  )
    return "foul!";
  else if ( sameNumberCard == 0 ) {
    std::sort(numArray, numArray + 5);
    bool straightFlag = this->isStraight( numArray );
    bool flushFlag = this->isFlush( suitArray );
    bool loyalFlag = this->isLoyal( numArray );
    
    if ( straightFlag ) {
      if ( flushFlag ) {
        if( loyalFlag )
          return "loyal straight flush";
        else
          return "straight flush";
      }
      else
        return "straight";
    }
    else if( flushFlag )
      return "flush";
    else
      return "boo";
  }
  else if ( sameNumberCard == 1 )
    return "one pair";
  else if ( sameNumberCard == 2 )
    return "two pair";
  else if ( sameNumberCard == 3 )
    return "three of a kind";
  else if( sameNumberCard == 4 )
    return "full house";
  else
    return "four of a kind";
}

const bool Poker::isFoul( Card hand[5] ) {
  bool sameCard = false;
  for ( int i = 0; i < 5; ++i ) {
    for ( int j = i+1; j < 5; ++j ) {
      sameCard = ( sameCard || ( hand[i].suit == hand[j].suit ) && ( hand[i].number == hand[j].number ) );
    }
  }
  
  bool notExistCard = false;
  for ( int i = 0; i < 5; ++i ) {
    notExistCard = notExistCard || ( hand[i].number < 1 || hand[i].number > 13 );
  }
  
  if ( sameCard || notExistCard )
    return false;
  else
    return true;
}

const int Poker::sameNumberCard ( const int numArray[5] ) {
  int sameNumberCard = 0;
  for ( int i = 0; i < 5; ++i ) {
    for ( int j = i+1; j < 5; ++j ) {
      if ( numArray[i] == numArray[j] ) {
        ++sameNumberCard;
      }
    }
  }
  return sameNumberCard;
}

const bool Poker::isStraight( const int sortedNumArray[5] ) {
    bool straightFlag = true;
      for ( int i = 0; i < 4; ++i ) {
      straightFlag = straightFlag && ( sortedNumArray[i] == sortedNumArray[i+1] - 1 );
    }
    return straightFlag || this-> isLoyal( sortedNumArray );
}

const bool Poker::isFlush( const Suit suitArray[5] ) {
      bool flushFlag = true;
    for ( int i = 0; i < 4; ++i ) {
      flushFlag = flushFlag && ( suitArray[i] == suitArray[i+1] );
    }
    return flushFlag;
}

const bool Poker::isLoyal( const int sortedNumArray[5] ) {
      bool loyalFlag = ( sortedNumArray[0] == 1 );
    for ( int i = 1; i < 5; ++i )
      loyalFlag = loyalFlag && ( sortedNumArray[i] == 10 + i - 1 );
    return loyalFlag;
}

プログラムの効率の最適化がなされていませんが、役を判定するメソッドのコードなどはストレートやフラッシュの真偽をメソッドに分離したおかげで読みやすくはなったのではないでしょうか。

ちなみに前回のコードで書いていたはずの役なしや反則の判定のテストが消えていました。

今回のコードを書くにあたって、Gitで59回コミットしました。何か変更するごとにコミットをしていたので、テストがレッドであろうと、グリーンであろうと変更した場合は全てコミットしていました。その場合に、いちいちコミットメッセージを考えるのはとても面倒なので、git-nowというツールを使っています。
git-nowは現在時刻をコミットメッセージとしてコミットします。そして、あとでrebaseオプションを用いて正式なコミットに変更できます。

これがあるおかげで、ペースを乱されずに細かく何度もコミットすることができます。TDDとSCMはかなり重要ですね。あと今回は使いませんでしたが、Jenkinsなどによる自動ビルド&テストも重要です。Jenkinsではコミットされるとビルドするような設定もできたはずですので、実際の開発ではこれを利用するのがいいと思います。(ただ、Jenkinsは重いのであまりスペックの高くないPCでは開発と同時に常時起動するのは厳しいかもしれません。)