zzzです。

 (2013年11月13日更新:あまりにソースが汚すぎるため、改めて一新して書き直しました。この記事に書いているので、こっちを見てください。ただC++のSTLのコンテナなどを多用しているため、その辺りの最低限の知識は必要となってきます。が確実に最新のほうを見たほうがいいです)

 たまにはまともに、C++でのポーカーの役判定の仕方について説明します。えー期待している方にはもうしわけありませんが、効率は甚だ悪い実装方法だと思います。動けばよいという感じです。

 せっかくですからソースファイルを公開しておきます。ただバグがあっても保障できかねますゆえ御了承ください。

https://box.yahoo.co.jp/guest/viewer?sid=box-l-hbklcdoyr77ihdmvgiqh73kqxm-1001&uniqid=8200d1e0-7b15-463c-9560-a07f26fe5008&viewtype=detail

 そもそもポーカーといっても色々なルールがあります。
 ここではカード5枚以下を手札として、カードの種類は同じカードが混ざっていてもよい、ジョーカーは何枚でもOKという割と一般的な設定で役判定をします。更に同じ役だった場合は、役に絡む数値から比較していき、数値の高いほうが勝利といたします(Aが最大,2が最小)。同役の場合マークの考慮は致しませんので引き分けの可能性があります。

 さて、役について色々と話す前に、そもそもカードとは何か?ということから決めていかなければなりません。これも色々と方針があるでしょうが、int型の数値で表現することにします。手札は

int cards[5]

 といった感じで表現します。さて、数字とカードの対応ですが、やはり色々やり方があると思いますが

   -1: カードなし
 0, 28: ジョーカー(黒, 赤)
 1~13:  スペードの2からA
15~27: クローバーの2からA
29~41:   ハートの2からA
43~55:   ダイヤの2からA

 とするのが一般的だと思います(マークは好きに入れ替えてよいですし、ジョーカーも増やしたり減らしたりしてよいでしょう)

 なぜならまず14で割った余りでカードに描かれたナンバーが分かります。(以下カードのナンバーという)
 さらに14で割って小数点を切り捨てた値がカードのマークになります。

 つまり上のようにカードの数値を決定すると

cards[0] % 14

 で数値が分かり

cards[0] / 14

 でマークが分かります。

 更にポーカーにおいて数字の1を2のカードに当てはめたほうが決定的に楽です。これに伴い数字の2を3のカードに、数字の3を4のカードに、……、数字の12をKのカードに、数字の13をAのカードに、とします。

 今後cardsに代入された値は先ほどの数値の表のいずれか(重複してもよい)とします。

 まだまだ準備が必要です。役判定をするにあたっては、カードをソートしないとかなり難しいです。従ってソートするアルゴリズムが必要です。

 えっ。「ソートのアルゴリズムなんて耳にタコだ!」ですって?確かに私も耳にタコが出るくらい聞いたことがありますが、ここでいうソートは「カードのナンバー順」にソートしたいのです。ちょっと意味が分からないかもしれないので解説します。

 たとえばcardsの中身が

 17, 12, 16, 22, 54

 とします。普通のソートでは、大きいものから順に並べると

 54, 22, 17, 16, 12

 となります。この場合カードのナンバーは

 Q, 9, 4, 3, K

 です。たとえ普通に並び替えたとしても、ポーカーの役判定に使うことは非常に難しいのです。そこでカードのナンバー順にソートして

 K, Q, 9, 4, 3

 といった並び順にしたいのです。これなら役判定にも困らなさそうですよね?


 さて、カードのナンバー順でのソートですが、普通のソート方式を知っていればたやすくできます。
 ここはソースコードを置いて、それを見てもらうということにします。バブルソートを使っています。


void SortCard(int* cards, int index)
{
std::vector indexs, copy_cards;
int i, save;
for(i = 0; i < index; ++i) indexs.push_back(i),
copy_cards.push_back(cards[i]);
int conti_flg = true;
while(conti_flg)
{
conti_flg = false;
for(i = 0; i < index - 1; ++i)
{
if((cards[indexs[i]] % 14 < cards[indexs[i + 1]]
% 14) || (cards[indexs[i]] % 14 == cards[indexs[i + 1]]
% 14 && cards[indexs[i]] > cards[indexs[i + 1]]))
{
save = indexs[i];
indexs[i] = indexs[i + 1];
indexs[i + 1] = save;
conti_flg = true;
}
}
}
for(i = 0; i < index; ++i) cards[i] = copy_cards[indexs[i]];
}


 cardsがカードのポインタ、indexはこのcardsの配列番号となります。
 まずindexsとcardsのコピーであるcopy_cardsを作り、indexsを入れ替えることによってcardsがカードのナンバー順になるような配列の並びを見つけ出し、あとはcardsに代入しているだけです。
 ついでに蛇足ですが、同じ数値の場合はマークの大小関係も比較しています。

 もっといいアルゴリズムが山のようにありそうですが、とりあえずこれで動くのでまあ問題ないです。ですよね?

 これでカードのナンバー順にソートができるようになりました。cardsはカードのナンバーが大きい順にソートされていることを仮定します。

 ではいよいよ役判定について。まず細かいルールをつめます。例えば

 相手が2と3のツーペア、ペアじゃないのが5
 こちらも2と3のツーペア、ペアじゃないのが8

 だったとしたら、勝敗判定として

役判定⇒ツーペアのうち強いほうの数値の比較⇒ツーペアのうち弱いほうの数値の比較⇒残りのペアじゃない数値の比較

 を行わなければなりません。この場合役は同じ⇒役の中で強い3も同じ⇒役の中で2番目の2も同じ⇒ペアじゃない5と8で決着が付き5 < 8なので自分が勝利

 といった流れになります。問題なのがこれをファイブカードからハイカードまできっちり判定しないといけないということです。

 とりあえずハイカード = 1, ワンペア = 2, ツーペア = 3, スリーカード = 4, ……, ストレートフラッシュ = 9, ロイヤルストレート = 10, ファイブカード = 11とします。

enum POKER_HANDS
{
HIGH_CARD = 1, ONE_PAIR, TWO_PAIR, THREE_CARD,STRAIGHT, FLUSH, FULL_HOUSE,
FOUR_CARD, STRAIGHT_FLUSH, ROYAL_STRAIGHT, FIVE_CARD, NONE
};

 
 NONEという仮想のものを入れていますが、これはエラーチェック用に使います。

 さて、いちいち役の中で最強の数値がどうのこうの、と判定しては面倒なので、DWORD型で表現します。(int型でもほとんどのPCは代用可能ですが)

 DWORD poker_hands;

 としましょう。

 DWORDの変数32ビットのうち、4ビットずつ区切り、20~23ビット目に役を代入し, 16~9ビット目に役の中での最強のカードのナンバーを代入し、12~15ビット目に役の中で2番目に強いカードのナンバーを代入し, 8~11ビット目に役の中で3番目に強いカードのナンバーを代入し、4~7ビット目に役の中で4番目に強いカードのナンバーを代入し、0~3ビット目に最も弱いカードのナンバーを代入します。

 まったく意味が分からないかと思われるので例をだしましょう。持ちカードが

 6, 5, 4, 3, 2

 だとします。この場合ストレートフラッシュとなっているためpoker_handsの中身を

 0x965432

 とする。ということになります。

 何が利点かといいますと、役の比較をする際大小関係だけで勝ち負けが分かります。
 例えば

 0x965432 対 0x9A9876

 の場合役は同じですが、最強の数値が6 < Aなので、⇒の勝ちです。これは

 0x965432 < 0x9A9876

 に相当しています。引き分けは==で分かります。

 さて、どのように勝敗を決めるかは分かりましたが、肝心の「代入方法」が分かっていません。がこの辺は私は泥縄のプログラマーなので、効率の良い方法がよくわかっていませんのでご容赦ください。

 とりあえず先にソースコードをおいておきます。


//DWORD = 32ビットなので、右から4ビットごとに
//第5番目に強いカード, 第4番目に強いカード,……, 第1番目に強いカード, 役と並べていく
//cardsはソートされていることが前提。
DWORD PokerHands(const int* cards)
{
int joker = 0;
bool straight_flg = true, flush_flg = true, fullhouse_flg = true;
POKER_HANDS ph;
DWORD ret = 0;
std::vector card_pairs[5];
std::vector::iterator it;
int i, j, k, l;
//役を決定する準備
for(i = j = 0; i < 4; ++i)
{
//カードが5枚に満たない場合ストレート、フラッシュはない、フルハウスはない
if(cards[i + 1] == -1) flush_flg = false,
straight_flg = false, fullhouse_flg = false;
//番兵, -1が出たら止まる
if(cards[i] == -1) break;
//ジョーカーが出た場合ジョーカーカウントを増やす
if(cards[i] % 14 == 0)
{
++joker;
continue;
}
//隣り合うものが同じだった場合、同じ数値がいくつあるのかをカウントしておく
if(cards[i] % 14 == cards[i + 1] % 14) ++j;
else
//隣り合うものが違う場合、カウント数に応じてカードの数値を保存しておき、カウントをリセットする
{
card_pairs[j].push_back(cards[i] % 14);
j = 0;
}
//フラッシュかどうか判定(-1(カードなし), 0(ジョーカー)のときは判定しない)
if(cards[i] / 14 != cards[i + 1] / 14
&& cards[i + 1] % 14 != 0) flush_flg = false;
}
//最後のカードがジョーカーでも番兵でもない場合、カードの数値を保存
if(cards[4] != -1 && cards[4] % 14 != 0)
card_pairs[j].push_back(cards[4] % 14);
//最後のカードがジョーカーだった場合ジョーカーカウントを増やす
if(cards[4] % 14 == 0) ++joker;
//ストレートかどうか判定, card_pairs[0]の最大要素と最少要素の差が4以下でかつ,
// card_pairs[0]以外の配列要素が空ならばストレートのフラグがたつ
if(!card_pairs[0].empty() &&
card_pairs[0][0] - card_pairs[0][card_pairs[0].size() - 1] <= 4)
{
for(i = 1; i < 5; ++i)
{
if(!card_pairs[i].empty())
{
straight_flg = false;
break;
}
}
}
else straight_flg = false;
//card_pairs[0]が空の場合でも、すべてジョーカーだった場合ストレートフラグがたつ
//が、ファイブカードと扱うので無視してよい

//ジョーカーの清算および役の数値の代入(キングのスリーカードなどの「キング」の部分の判定)
bool first_flg = true;
for(i = 4, l = 4, k = -1; i >= 0; --i)
{
if(card_pairs[i].empty()) continue;
else
{
//初めてここに来た場合、ジョーカー分を清算する
if(first_flg)
{
first_flg = false;
k = i + joker;
}
for(it = card_pairs[i].begin();
it != card_pairs[i].end(); ++it, --l) ret |= *it << l * 4; //役の数値を代入
}
}
//あとは役を決める, k == -1のときは手札がジョーカーのみ
if(k == 4 || joker == 5) ph = FIVE_CARD;
//joker5枚の場合, card_pairsには何も代入されない
else if(straight_flg && flush_flg &&
!card_pairs[0].empty() && card_pairs[0][card_pairs[0].size() - 1] >= 9)
ph = ROYAL_STRAIGHT;
//カードペアが空の場合でロイストならジョーカー5枚、よってファイブカードしかありえない
else if(straight_flg && flush_flg) ph = STRAIGHT_FLUSH;
else if(k == 3 || joker == 4) ph = FOUR_CARD;
//手札がちょうどjoker4枚だけの場合, フォーカードとして扱う
else if(k == 2 && card_pairs[0].size() != 2 &&
fullhouse_flg && joker <= 1) ph = FULL_HOUSE;
//ジョーカー2枚以上でフルハウスはありえないのでジョーカー1枚以下確定
else if(flush_flg) ph = FLUSH; //フラッシュ確定
else if(straight_flg) ph = STRAIGHT; //ストレート確定
else if(k == 2 || joker == 3) ph = THREE_CARD;
//スリーカード, ジョーカー2枚でもありえるがこれでよい
else if(card_pairs[1].size() == 2) ph = TWO_PAIR;
//ツーペアはジョーカーが一枚でもあれば起こりえない
else if(k == 1 || joker == 2) ph = ONE_PAIR;//ワンペア確定
else if(k == 0 || joker == 1) ph = HIGH_CARD; //ハイカード確定
else ph = NONE; //一応バグチェック

ret |= ph << 20; //役を代入

//あとはすべてジョーカーだったとき、役の数値をどうするかが問題である。
//大富豪のルールなどではジョーカーが含まれていれば最強のため,
//最強の数値を放り込めばよいのだが,
//ポーカーの場合どうするかはよくわからない。とりあえずジョーカーのみの場合
//エースとして判定する, Aより強くしたい場合は14を代入すればよい
if(k == -1) ret |= 13 << 16;
//ストレートの場合のretの代入がうまくいっていないので代入する
if(ph == STRAIGHT || ph == STRAIGHT_FLUSH || ph == ROYAL_STRAIGHT)
{
ret &= 0xF00000;
int size = card_pairs[0].size();
j = 0;
for(i = 0; i < size - 1; ++i)
{
j += card_pairs[0][i] - card_pairs[0][i + 1] - 1;
}
for(i = 0; i < 5; ++i) ret |=
(min(13, card_pairs[0][0] + joker - j) - i) << (4 - i) * 4;
}

return ret;
}


 えーかなり混沌としたソースですが、基本的な発想は原始的です。

bool straight_flg = true, flush_flg = true, fullhouse_flg = true;

 によってまずストレートのフラグ、フラッシュのフラグ、フルハウスのフラグを立てます。ストレートじゃなかったりした場合falseを代入するわけです。次に役を決定する準備をします。


//役を決定する準備
for(i = j = 0; i < 4; ++i)
{
//カードが5枚に満たない場合ストレート、フラッシュはない、フルハウスはない
if(cards[i + 1] == -1) flush_flg = false,
straight_flg = false, fullhouse_flg = false;
//番兵, -1が出たら止まる
if(cards[i] == -1) break;
//ジョーカーが出た場合ジョーカーカウントを増やす
if(cards[i] % 14 == 0)
{
++joker;
continue;
}
//隣り合うものが同じだった場合、同じ数値がいくつあるのかをカウントしておく
if(cards[i] % 14 == cards[i + 1] % 14) ++j;
else
//隣り合うものが違う場合、カウント数に応じてカードの数値を保存しておき、カウントをリセットする
{
card_pairs[j].push_back(cards[i] % 14);
j = 0;
}
//フラッシュかどうか判定(-1(カードなし), 0(ジョーカー)のときは判定しない)
if(cards[i] / 14 != cards[i + 1] / 14
&& cards[i + 1] % 14 != 0) flush_flg = false;
}
//最後のカードがジョーカーでも番兵でもない場合、カードの数値を保存
if(cards[4] != -1 && cards[4] % 14 != 0)
card_pairs[j].push_back(cards[4] % 14);
//最後のカードがジョーカーだった場合ジョーカーカウントを増やす
if(cards[4] % 14 == 0) ++joker;
//ストレートかどうか判定, card_pairs[0]の最大要素と最少要素の差が4以下でかつ,
// card_pairs[0]以外の配列要素が空ならばストレートのフラグがたつ
if(!card_pairs[0].empty() &&
card_pairs[0][0] - card_pairs[0][card_pairs[0].size() - 1] <= 4)
{
for(i = 1; i < 5; ++i)
{
if(!card_pairs[i].empty())
{
straight_flg = false;
break;
}
}
}
else straight_flg = false;


 やってることはコメントにも書かれてあるように

・カードが5枚以下ならストレート、フラッシュ、フルハウスのフラグをfalseにしています。

・ジョーカーの枚数のカウントを行っています。

・同じカードのナンバーの組を、そのカードの枚数によって代入箇所を変えて保持します。例えば

4, 18, 32, 6, 20

 とカードを持っている場合3枚そろっている組という意のcard_pairs[2]に4を代入し、二枚そろっている組という意のcard_pairs[1]に6を代入します。

・フラッシュ判定です。一番簡単です。

 だけです。はっきりいってこれだけでもうほとんど終わりですがストレートだけ判定しないといけません。

 ストレートは一枚しかそろってないカードのうち、最大のナンバー引く最小のナンバーが4以下 かつ カードが5枚 かつ 二枚、三枚、四枚、五枚のカードのナンバーがそろっている組が存在しない、で判定できます。

 これで大体終わっています。ただジョーカーを何枚でも良かったり、カード枚数が3枚とかいう可能性があるためやけに細々したソースになってしまいました……。

//ジョーカーの清算および役の数値の代入(キングのスリーカードなどの「キング」の部分の判定)
bool first_flg = true;
for(i = 4, l = 4, k = -1; i >= 0; --i)
{
if(card_pairs[i].empty()) continue;
else
{
//初めてここに来た場合、ジョーカー分を清算する
if(first_flg)
{
first_flg = false;
k = i + joker;
}
for(it = card_pairs[i].begin();
it != card_pairs[i].end(); ++it, --l) ret |= *it << l * 4; //役の数値を代入
}
}


 ジョーカーの清算を行っています。「ストレート、フラッシュ系の役以外、一番強い数値と同じカードにジョーカーがなる」ということを利用しています。
 プログラム上のkの値が、ジョーカーを込めて「何枚の同じカードのナンバーの組」のうちの「何枚」の最大の値となります。

 ついでにここでDWORDの値に先ほどお話したビットごとに分けた数値の強さを代入しておきます。cardsがソートされていることが前提にあるので上のような書き方で大丈夫です。

あとは役を代入するだけです。


//あとは役を決める, k == -1のときは手札がジョーカーのみ
if(k == 4 || joker == 5) ph = FIVE_CARD;
//joker5枚の場合, card_pairsには何も代入されない
else if(straight_flg && flush_flg &&
!card_pairs[0].empty() && card_pairs[0][card_pairs[0].size() - 1] >= 9)
ph = ROYAL_STRAIGHT;
//カードペアが空の場合でロイストならジョーカー5枚、よってファイブカードしかありえない
else if(straight_flg && flush_flg) ph = STRAIGHT_FLUSH;
else if(k == 3 || joker == 4) ph = FOUR_CARD;
//手札がちょうどjoker4枚だけの場合, フォーカードとして扱う
else if(k == 2 && card_pairs[0].size() != 2 &&
fullhouse_flg && joker <= 1) ph = FULL_HOUSE;
//ジョーカー2枚以上でフルハウスはありえないのでジョーカー1枚以下確定
else if(flush_flg) ph = FLUSH; //フラッシュ確定
else if(straight_flg) ph = STRAIGHT; //ストレート確定
else if(k == 2 || joker == 3) ph = THREE_CARD;
//スリーカード, ジョーカー2枚でもありえるがこれでよい
else if(card_pairs[1].size() == 2) ph = TWO_PAIR;
//ツーペアはジョーカーが一枚でもあれば起こりえない
else if(k == 1 || joker == 2) ph = ONE_PAIR;//ワンペア確定
else if(k == 0 || joker == 1) ph = HIGH_CARD; //ハイカード確定
else ph = NONE; //一応バグチェック

ret |= ph << 20; //役を代入


 高い役から順番にチェックするのが簡単でしょう。

 kが4のときは手持ちに5枚カードのナンバーが同じ組を持っているので、ファイブカード確定です。
 また手札が全てジョーカーの場合、仕様上card_pairsのどの配列にもカードが代入されないため、jokerが5枚のときも判定しないといけません。

 ロイヤルストレートですが、ストレートフラグかつフラッシュフラグが立っていて、1枚カードのナンバーが同じ組のうち、最小の数値が9を超えていればロイヤルストレートです。

 ストレートフラッシュは、ロイヤルストレートを判定してからもう一度ストレートフラグかつフラッシュフラグが立っているか確認し、立っているならストレートフラッシュです。

 フォーカードはファイブカードと同じくkが3のときまたはジョーカー4枚のときフォーカードです。

 フルハウスは、ジョーカー1枚以下じゃないと成立しません。(ジョーカー2枚以上でフルハウスの条件を満たすとフォーカード以上が確定する)
 k=2で3枚そろった組が存在しています。またフルハウスフラグが経っているのも確認するのでカードは5枚あります。またその中でスリーカードとなりえる条件が

card_pairs[0].size() == 2

 しかありえません。(== 1ならフォーカードなのでおかしい, == 0ならば2枚カードのナンバーが同じ組が存在しているのでフルハウス)

 よって

card_pairs[0].size() != 2

 で判定しても大丈夫です。

 次にフラッシュ、ストレートをフラグが立っているならフラッシュ、ストレートを確定させておきます。

 スリーカードは、フルハウスのときをすでに考えたので、k == 3またはジョーカー3枚のときだけを判定すればよいです。

 ツーペア以下になるともはやジョーカーについてはほとんど考慮しなくて良いので楽です。普通にカードの組の値をカウントすれば問題ありません。

 あとはDWORD型にこの役を代入して終わり……といきたいのです。がストレートの場合に代入する強さ順の数値が間違っています。ジョーカーを考慮していません。従ってジョーカーを考慮しつつ、強さ順に数値を代入していきます。

 また全てジョーカーだった場合のときも数値を代入していません。この数値をどう代入するかでジョーカーの強さを決めます。とりあえずAと同じ強さであるという風にしてみましたが数値を弄ればAより強くなります。


//あとはすべてジョーカーだったとき、役の数値をどうするかが問題である。
//大富豪のルールなどではジョーカーが含まれていれば最強のため,
//最強の数値を放り込めばよいのだが,
//ポーカーの場合どうするかはよくわからない。とりあえずジョーカーのみの場合
//エースとして判定する, Aより強くしたい場合は14を代入すればよい
if(k == -1) ret |= 13 << 16;
//ストレートの場合のretの代入がうまくいっていないので代入する
if(ph == STRAIGHT || ph == STRAIGHT_FLUSH || ph == ROYAL_STRAIGHT)
{
ret &= 0xF00000;
int size = card_pairs[0].size();
j = 0;
for(i = 0; i < size - 1; ++i)
{
j += card_pairs[0][i] - card_pairs[0][i + 1] - 1;
}
for(i = 0; i < 5; ++i) ret |=
(min(13, card_pairs[0][0] + joker - j) - i) << (4 - i) * 4;
}

return ret;
}


 これでようやく終わりです。……ほめられたアルゴリズムではないでしょうから、もっと良い方法もあるのかもしれません。何かあるという方はぜひ教えてください。


Secret

TrackBackURL
→http://mathyippee.blog.fc2.com/tb.php/98-049b7946