What is it, naokirin?

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

前回、ポーカーの役を数値のみで判定していたので、少し進めてスートでも判定できるように構造体と列挙体を用いて改良することにしました。その過程で前回のテストに足りていなかった境界値が存在することに気がつきました。その辺りは後で書くとして、今回のコードです。

PokerTest.cpp

#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
#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));
}

Poker.h & Poker.cpp

/* Poker.h */
#include <string>

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

struct Card {
  Suit suit;
  int number;
};

struct Poker {
  Poker();
  ~Poker();
  
  std::string pokerHands(Card array[5]);
};

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

Poker::Poker() {}

Poker::~Poker() {}

std::string Poker::pokerHands( Card array[5] ) {
  
  int numArray[5], suitArray[5];
  for( int i = 0; i < 5; ++i ){
    numArray[i] = array[i].number;
	suitArray[i] = array[i].suit;
  }

  int sameCard = -5;
  for ( int i = 0; i < 5; ++i ) {
    for ( int j = i; j < 5; ++j ) {
      if ( numArray[i] == numArray[j] ) {
	    ++sameCard;
      }
    }
  }
  
  if ( sameCard == 0 ) {
    std::sort(numArray, numArray + 5);
    bool straightFlag = true, flushFlag = true;
    for ( int i = 0; i < 4; ++i ) {
      straightFlag = straightFlag && ( numArray[i] == numArray[i+1] - 1 );
      flushFlag = flushFlag && ( suitArray[i] == suitArray[i+1] );
    }
    bool loyalFlag = ( numArray[0] == 1 );
    for ( int i = 1; i < 5; ++i )
      loyalFlag = loyalFlag && ( numArray[i] == 10 + i - 1 );

    if ( straightFlag || loyalFlag ) {
      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 ( sameCard == 1 )
    return "one pair";
  else if ( sameCard == 2 )
    return "two pair";
  else if ( sameCard == 3 )
    return "three of a kind";
  else if( sameCard == 4 )
    return "full house";
  else if ( sameCard == 6 )
    return "four of a kind";
  else
    return "foul!";
}

少々メソッド一つに収めるには実装が肥大化してます。実はまだリファクタリングをちゃんとかけてません。「思いついた実装」をしている状態です。
このコードはもう一回記事として載せるつもりで、その時にはカード自体が存在していい組み合わせか、また数字が1〜13に収まっているかなどの判定とリファクタリングを行おうと思っています。

リファクタリングとしてはメソッドの分割もですが、テスト自体も少し分割をした方がいいように思えます。テスト一つに対して、あまりにテスト項目が多過ぎるように思えます。

さて、前回のコードにあった境界値の条件が抜け落ちていてバグコードになっていた理由ですが、ストレートの条件が足りていなかったことです。ストレートは単純に数字が並んでいるだけでなく、[10, 11, 12, 13, 1]という並びでも成立します。今回ロイヤルストレートフラッシュの判定を追加したときに発生したフラッシュの判定のテストの失敗で気がついたのですが、このあたりでもテスト駆動開発のおかげでバグの発見がスムーズになってます。このあたりのテストの境界値選びには気をつけなければなりませんね。(三角測量を少々怠った罰かもしれません。大丈夫だろうと思っていたのですが…三角測量は重要ですよ、私みたいに失敗しないために)。

後はSCM(今回はGit)を用いることで細かく作業状況を記録でき、かつTDDで一つ一つの実装が確かに成功したことを確認しつつ進めるのはかなり便利かと思います。