【C言語】ポーカーの作り方

ポーカーの作り方の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、トランプゲームの1つである「ポーカー」の作り方の解説と、その「ポーカー」をC言語で実装したサンプルプログラムの紹介をしていきたいと思います。

作成するポーカー

まず最初に、今回作成するポーカーがどのようなものであるかについて紹介していきます。

ポーカーの実行画面

今回作成するプログラムの実行画面は下記のようになります(スマホで見るとズレるかも…)。

[あなたの手札]
  S:2  C:A  C:2  D:A  H:A  

[カードの交換]
  1枚目のカード(S:2)を交換しますか?(y/n)y
  2枚目のカード(C:A)を交換しますか?(y/n)n
  3枚目のカード(C:2)を交換しますか?(y/n)y
  4枚目のカード(D:A)を交換しますか?(y/n)n
  5枚目のカード(H:A)を交換しますか?(y/n)n

[あなたの手札]
  D:2  C:A  S:A  D:A  H:A  

[あなたの手札の役]
  フォーカード

トランプの図などを表示すると大変なので、コンソールで全てプレイできるように全て文字だけでポーカーを再現するようにしています。

[あなたの手札] の下の行に表示されているものがトランプのカードを表示しており、: の前側がカードのマークを、: の後ろ側がカードの数字をそれぞれ表しています。

カードのマークは4種類で、それぞれのマークを下記のようにアルファベット1文字で表現しています。

  • ダイヤ:D
  • ハート:H
  • スペード:S
  • クラブ:C

カードの数字は全部で13種類です。 2〜10のものはそのまま数値を表示しますが、1と11〜13に関しては下記のようにアルファベットで表現しています。 

  • 1:A
  • 11:J
  • 12:Q
  • 13:K

スポンサーリンク

カードの枚数は52枚(ジョーカーは無し)

今回作成するポーカーでは、”ジョーカーは無し” としたいと思います。これは、ジョーカーがあると役の判定が難しくなるためです。

私が楽をしたいというのも理由の1つですが、この方がソースコードが簡潔になるので、読んでいる方にもプログラムで何が行われているのかが分かりやすくなると思います。そのため、まずはジョーカーなしで作成することにしました。

ジョーカー無しなので、使用するカードの枚数は52枚となります(4種類のマーク×13種類の数字)。

1人プレイ専用

また、今回作成するプログラムでプレイできるポーカーは一人プレイ専用となります。他の人との対戦やコンピュータの対戦は行えませんのでご了承ください。

役は10種類

さらに、今回のプログラムでは、ポーカーの役は下記の10種類のみとします(ノーペアは “役なし” なので、実質9種類になります)。

  • ロイヤルストレートフラッシュ
  • ストレートフラッシュ
  • フォーカード
  • フルハウス
  • フラッシュ
  • ストレート
  • スリーカード
  • ツーペア
  • ワンペア
  • ノーペア

大体オーソドックスなものは揃っていると思いますが、ファイブカードなどは役として存在しないので注意してください(そもそもジョーカーがないのでファイブカードになり得ない)。

スポンサーリンク

ゲームの流れ

今回作成するプログラムを実行するとポーカーが開始します。ポーカー開始後、プレイヤー目線では下記のような流れでゲームが進みます。

  • カードが配られる
  • 手札のカードを交換する
  • 手札の役が発表される

カードが配られる

まずゲームを開始すると、ランダムにカード5枚が配られます。コンソールには、配られたカードが下記のように表示されます。

[あなたの手札]
  S:2  C:A  C:2  D:A  H:A  

ポーカーにおいて、5枚のカードは当然重複なしで配られますので、今回のプログラムでも5枚のカードは重複なしで配るようにしています。

手札のカードを交換する

カードが配られた後は、カードの交換フェーズに移ります。

下記のように1枚ずつ交換するかどうかを尋ねるようにしており、y を入力することでそのカードを交換することができます。n が入力された場合は、そのカードの交換は行われません。

[カードの交換]
  1枚目のカード(S:2)を交換しますか?(y/n)y
  2枚目のカード(C:A)を交換しますか?(y/n)n
  3枚目のカード(C:2)を交換しますか?(y/n)y
  4枚目のカード(D:A)を交換しますか?(y/n)n
  5枚目のカード(H:A)を交換しますか?(y/n)n

カードを交換することを選択したカードは捨てられ、捨てられた分のカードが新たにランダムに配られます。

そして、配られた後の手札が再び下記のように表示されます。

[あなたの手札]
  D:2  C:A  S:A  D:A  H:A  

上記の例では1枚目と3枚目のカードのみの交換を行なっていますので、左から1番目と3番目のカードのみが、最初にカードが配られた時の手札から変化していることが確認できると思います。

手札の役名が表示される

カードの交換が終わると、手札の5枚のカードから役を判断され、その役名が表示されてゲームが終了します。

[あなたの手札の役]
  フォーカード

この役名は、役は10種類で紹介した役のうちの1つが表示されるようにしています。役なしの場合は “ノーペア” と表示されます。

ポーカーの作り方

ここまでの説明で、どのようなプログラムを作成していくかのイメージは付いてきたのではないかと思います。

次は、このポーカープログラムのC言語での作り方について解説していきたいと思います。

ポーカープログラムを作成する上で必要になる処理は、大きく分けて下記の5つになると思います。

  • カードを用意する
  • カードを表示する
  • カードを配る
  • カードを交換する
  • 役を判断して役名を表示する

ですので、この5つの処理それぞれについて、C言語において上記をどうやって実現するのかについて解説していきたいと思います。

カードを用意する

まずポーカーを行うのですから、カードを用意する必要があります。

ということで、まずはC言語プログラム上で “カードを用意する” ためには、どのようなことを行う必要があるのかについて解説していきます。

カードを表現する型の定義

まず前提として、C言語の基本的な型(int や double など)では単なる数値を表現するものしかなく、当然ながらカードを表現する型はありません。

ですので、今回は “構造体” と typedef を利用してカードを表現する型を自作します。

具体的にカードを表現する型の例は下記のようになります。

カードの型
/* カードを表す構造体 */
typedef struct _CARD {
    int suit; /* マークを表す数値(0 - 3) */
    int number; /* 数字を表す数値(1 - 13) */
} CARD;

ここで定義した CARD がカードを表す型となり、メンバ suit がカードのマークを、メンバ number がカードの数字をそれぞれ表します(トランプのカードの図柄の英訳は “suit” らしいです)。

MEMO

suit や number は単なる整数を格納するメンバですので、そのまま表示すると当然単なる整数として表示されてしまいます

ただ、作成するポーカーで説明したようにマークや特定の数字はアルファベットで表示したいので、表示する前に suit や number をアルファベットに変換してから実際の表示を行うようにします

これに関しては、次のカードを表示するで解説します。

カードを表す変数の宣言

上記の構造体の定義によりカードを表す型 CARD が用意できますので、後はこの CARD 型でサイズ 52 の配列を変数宣言すれば、52枚のカードを表現する要素が用意できることになります。

カードを用意する
CARD cards[52]; /* カード */

カードへのマークと数字の割り当て

ただし、変数宣言直後だとカードにマークや数字が割り振られていないので、全てのカードはまだのっぺらぼうの状態です。

なので、次はカードにマークと数字を割り振っていく処理を行っていきます。

ポーカーで扱うカードには重複したカード(同じマーク&同じ数字のカード)は存在しませんので、それに倣って重複したカードができないようにマークと数字を割り振っていく必要があります。

これは、下記のような2重ループの中で、各カード cards の suit と number に “マークを表す整数” と “数字を表す整数” をそれぞれ代入していくことで実現することができます。

マークと数字の割り振り
/* カードの初期化 */
for (j = 0; j < 4; j++) {
    for (i = 0; i < 13; i++) {

        /* マークを設定 */
        cards[j * 13 + i].suit = j;

        /* 数字を設定 */
        cards[j * 13 + i].number = i + 1;
    }
}

これにより cards の先頭の要素から順に、下記のようにカードが並ぶようになります。

  • cards[0]:suit=0 number=1
  • cards[1]:suit=0 number=2
  • cards[2]:suit=0 number=3
  • ・・・・・・ 略 ・・・・・・
  • cards[49]:suit=3 number=11
  • cards[50]:suit=3 number=12
  • cards[51]:suit=3 number=13

以上により、マークや数字が割り振られたカード52枚が配列 cards として用意されたことになります。

今後、cards[0] が一番上に積まれたカードとし、添字の小さい方から順にカードが積まれているものとして扱っていきます(一番下のカードは cards[51])。

また、ここで利用した “構造体” と typedef については下記ページで解説していますので、詳しく知りたい方は下記ページを読んでみていただければと思います。

構造体解説ページのアイキャッチ 【C言語】構造体について初心者向けに分かりやすく解説 typedef解説ページのアイキャッチ C言語のtypedefについて具体例を用いて分かりやすく解説

スポンサーリンク

カードを表示する

ただ、プログラム内部で扱うデータとしては上記の cards の suit や number で問題ないですが、このまま表示するとトランプのカードっぽくないです。マークも整数で表されていますし…。

もうちょっとトランプっぽくカードを表示するため、画面にカードを表示する際に、cards の suit と number をトランプのマークや数字の表記に合わせて “文字列” に変換してから表示するようにしたいと思います。

より具体的には、各整数を下記のように文字列に変換して表示するようにします。

  • suit:
    • 0 → "D"
    • 1 → "H"
    • 2 → "S"
    • 3 → "C"
  • number:
    • 1 → "A"
    • 2 〜 10 → そのまま
    • 11 → "J"
    • 12 → "Q"
    • 13 → "K"

このような整数から文字列への変換は、あらかじめ配列で下記のような変換テーブルを用意しておくことで簡単に実現することができます。

表示文字列への変換テーブル
/* マークを表す文字列の配列 */
char *suit_str[4] = {
    "D", "H", "S", "C"
};

/* 数字を表す文字列の配列 */
char *number_str[14] = {
    "0", "A", "2", "3", "4", "5", "6",
    "7", "8", "9", "10", "J", "Q", "K"
};

上記の配列の添字に cards の suit や number を指定することで、それらの変換先の文字列が取得可能です(もうちょっと正確にいうと、変換先の文字列の “アドレス” が取得可能です)。

例えば、suit_str[3] では suit が 3 の時の変換先の文字列 "C" が取得できますし、number_str[1] では number が 1 の変換先の文字列 "A" を取得することができます。

また、suit_str[cards[i].suit] と number_str[cards[i].number] により cards[i] のマークと数字を文字列として取得することができますので、その取得した文字列を下記のように printf で表示すれば、作成するポーカーで紹介したようなカードの表示を行うことができます。

カードの表示
printf("%s:%s", suit_str[cards[i].suit], number_str[cards[i].number]);
MEMO

本当は文字列ではなく “文字” として扱った方がシンプルにプログラミングできます

ですが、数字が 10 の時には "10" と “2文字” で表示する必要があり、1文字では扱いきれないので断念しました…

カードを配る

次は、カードを用意するで用意したカードをプレイヤーに配る処理を実現していきたいと思います。

このカードを配る処理でポイントになるのが下記の2点になります。

  • ランダムにカードを配る
  • 重複なくカードを配る

この2つを簡単に実現するために、今回はまずカードを用意するで用意したカードを “ランダムにシャッフル” し、その後に “一番上のカードから配る” ようにしたいと思います。

カードをランダムにシャッフルする

カードのシャッフルなど、データの並びをランダムにするためのアルゴリズムとして Fisher–Yates shuffle というものがあります。

このアルゴリズムを利用した場合、カードのシャッフルは下記のような処理で実現することができます。

カードをシャッフル
/* シャッフル範囲の末尾を設定 */
i = 52 - 1;

while (i > 0) {
    /* シャッフル範囲(0〜i)からランダムに1つデータを選択 */
    j = rand() % (i + 1);

    /* ランダムに決めたデータとシャッフル範囲の末尾のデータを交換 */
    tmp = cards[j];
    cards[j] = cards[i];
    cards[i] = tmp;

    /* シャッフル範囲を狭める */
    i--;
} 

上記の処理で具体的にどんなことが行われているのかを知りたい方は、下記ページの 手順2:データ列をシャッフルする(Fisher–Yates shuffle) で解説していますので、こちらを参照していただければと思います。

C言語で重複なしの乱数を生成する方法解説ページアイキャッチ 【C言語】重複なしで乱数を生成する方法

また、ここで使用した rand 関数については下記ページで解説していますので、詳しく知りたい方は下記ページを参考にしていただければと思います。

C言語で乱数を扱う方法の解説ページアイキャッチ C言語で乱数を扱う方法(rand関数とsrand関数)

上記ページでも解説していますが、プログラムを実行するたびに取得できる乱数を異なるものにしたい場合は、rand 関数を実行する前に srand 関数を実行する必要があります。

一番上のカードから配る

カードをシャッフルすることによりカードの並びがランダムになりますので、1番上に積まれたカード(つまり cards[0])から順に5枚カードを配るだけで、ランダムに5枚のカードを配ることができます。

また、カードを用意するで紹介した方法で cards の各要素のカードが重複しないようにマークと番号を割り振っておけば、重複なしでカードを配ることも実現することができます。

この “一番上のカードを配る” 処理は下記のようになります。

カードを配る
for (i = 0; i < 5; i++) {

    /* 配られたカードを手札のカードとしてコピー */
    hand[i] = cards[top];

    /* 次に配るカードを新たな1番上のカードに設定 */
    top++;
}

top と hand という変数がいきなり出てきたので説明しておきます。

top は、配っていないカードのうち、”一番上にあるカードの位置” を示す変数です。カードを配る際には、この位置のカード、つまり cards[top] から配るようにしています。

最初にカードを配るときは cards[0] から順に配るため、わざわざ配るカードの位置を変数で管理する必要はないです。ですが、次にカードを配るとき、具体的にはカード交換時にカードを配るときには、カードを配り始める位置がどの位置であるかを知っておく必要があります(特に2回目以降の交換時)。

その位置が分かるように、この変数 top を用意し、カードを配るたびに top++ するようにしています(最初にカードを配るときは cards[0] から配るように、top の値は事前に 0 に設定しておく必要があります)。

また、hand は配られたカード、つまり手札のカードを管理する CARD 型の配列です。手札のカードは5枚ですので、配列 hand のサイズも 5 となります。

手札のカード
CARD hand[5]; /* 手札のカード */

配られた5枚のカードを、それらのカードがどのマークのどの数字のものであるかを hand の各要素に格納して覚えておくようにしています。 

これを hand[i] = cards[top] により行っています。構造体の代入ではメンバの値のコピーが行われますので、これにより配られたカードのマークと数字が hand の各要素にコピーされることになります。

以降で説明する “カードの交換” や “役の判断” の際には、この hand の各要素を参照しながら処理を行います。

MEMO

構造体型の変数のコピーは構造体のサイズが大きいと処理時間が長くなります

ですので、本当は上記のような構造体の変数のコピーはせずに、ポインタで変数を指すだけにした方が良いです

ただ、今回は “簡単のため&構造体のサイズが大きくない” という理由から構造体の変数のコピーをするようにしています

カードのシャッフルの必要性

ここまでの説明を聞いて、カードをシャッフルしなくても、カードを配る位置さえランダムに決めれば良いのではないかと感じた方も多いかもしれません。

カードを配る時にランダムにカードを選ぶ様子

もちろんそれでもランダムにカードを配ることはできます。

ただ、乱数は重複することもありますので、すでに配り終わったカードの位置を覚えておかないと、重複して同じカードを配ってしまう可能性があります。

例えば、カードを重複なしに配るためには、下記のような処理を行う必要があります。

  • 配り終わったカードの位置を配列などで覚えておく
  • ランダムに決めた位置のカードが既に配り終わったものである場合、再度ランダムに配るカードの位置を決める

その一方で、カードのシャッフルを行って “重複無しでランダムに並んだカードの配列” を作成しておけば、後は配列の先頭側からカードを配るだけで “ランダム” かつ “重複なし” にカードを配ることが可能です。

なので、私はこっちの方がプログラミングが楽だと考え、今回はカードをシャッフルするようにしています。

カードを交換する

続いてカードを交換する処理を実現する方法について解説します。

まず、どのカードを交換するのかを知るために、プレイヤーから交換したいカードの指定を受け付ける必要があります。

交換するカードの指定を受け付ける

この交換するカードの指定を受け付ける処理は、単純に scanf を利用することで実現できます。指定の受け付け方は様々で、交換を行うカード全ての指定を1度に受け付けるようなこともできると思います(例えば1枚目と3枚目と4枚目を交換したい場合に 134 を入力してもらうなど)。

ただ、今回は簡単のため、1枚1枚プレイヤーに交換するかどうかを YES or NO で尋ねることで、交換するカードの指定を受け付けるようにしたいと思います。

この交換するカードの指定を受け付ける処理は、例えば下記のように実現することができます。

交換するカードの指定の受付
for (i = 0; i < 5; i++) {
    /* y/nのどちらかが入力されるまでループ */
    while (1) {

        /* カードを交換するかどうかの入力を受け付ける */
        printf("  %d枚目のカード(%s:%s)を交換しますか?(y/n)",
            i + 1, suit_str[hand[i].suit], number_str[hand[i].number]);
        scanf("%s", input);

        if (strcmp(input, "y") == 0) {
            /* i枚目を交換することを覚えておく */
            exchange[i] = 1;
            break;
        } else if (strcmp(input, "n") == 0) {
            /* i枚目を交換しないことを覚えておく */
            exchange[i] = 0;
            break;
        }
        printf("  yもしくはnを入力してください!!!\n");
    }
}

手札の5枚のカード1枚1枚に対して、YES を表す y か NO を表す n の入力を受け付けています。

y が入力された場合には、そのカードを交換することを覚えておくために、配列 exchange に 1 を格納するようにしています。

指定されたカードを交換する

交換するカードの指定を受け付けた後は、その指定された手札のカードの交換を行います。

これは、指定されたカードを手札から捨て、新たに配られたカードを手札に入れることで実現することができます。

例えば、指定された手札のカードが hand[i] である場合、この hand[i] を次に配るカードである cards[top] で上書きすれば、元々の手札のカード hand[i] が捨てられ、新たなカード cards[top] が手札に入ってきたことになります。つまり、これによりカードが交換されたことになります。

このような処理を指定されたカード全てに対して行えば、ユーザーが指定したカードの交換が達成されることになります。

これを行う処理の例が下記のようになります。

指定されたカードの交換
for (i = 0; i < 5; i++) {
    if (exchange[i] == 1) {
        /* 交換を指定されたカードの場合 */

        if (top >= 52) {
            printf("  カードが足りなくて%d枚目のカードは交換できませんでした...\n", i + 1);
            continue;
        }

        /* 一番上のカードを取って手札のカードと入れ替える */
        hand[i] = cards[top];
        top++;
    }
}

top がカードの枚数 52 以上になった場合、全てのカードを配り終わったことになるため、もうカードの交換は行えません(top はまだ配ってないカードのうち、一番上側に積まれているカードの位置を示す変数なので、52 以上になったら全てのカードを配り終えたことになります)。

ですので、この場合は交換は行わず、その旨を printf で表示するだけにしています。

スポンサーリンク

役を判断して役名を表示する

カードの交換が終わったら、次は役の判断を行います。

手札のカードを示す hand の各要素のマークと数字を1つ1つ確認していけば、その手札がどの役を満たしているかは判断することができます。が、かなり処理が複雑になります。

できるだけ簡単に役を判断するためには、役を判断する前に、手札のカードの “各マーク” と “各数字” の枚数を集計しておくのが良いと思います。

これらを集計することで、役を判断する時に下記のようなメリットを得ることができます。

  • 手札のカードの並びを考慮する必要がない
  • 集計結果を参照するだけで、手札に各マーク・各数字のカードが何枚あるかが分かる

手札のカードを集計する

ということで、今回も手札のカードの集計を行ってから役の判断を行うようにしたいと思います。

この集計は、単に手札のカードの5枚に各マーク・各数字のカードが何枚あるかを数えることで実現することができます。

例えば手札のカードが下記の5枚であるとすれば、

[あなたの手札]
  D:2 C:A S:A C:2 H:A

各マークと各数字の枚数は下記のようになります。

  • マーク
    • ダイヤ:1枚
    • ハート:1枚
    • スペード:1枚
    • クラブ:2枚
  • 数字
    • 1:3枚
    • 2:2枚
    • それ以外:0枚

このような集計は、下記のような処理により実現することができます。

手札カードの集計
/* 各々のマークの枚数を0に初期化 */
for (i = 0; i < 4; i++) {
    count_suit[i] = 0;
}

/* 各々の数字の枚数を0に初期化 */
for (i = 1; i < 14; i++) {
    count_number[i] = 0;
}

/* 各々のマークと数字の枚数をカウント */
for (i = 0; i < 5; i++) {
    count_suit[hand[i].suit]++;
    count_number[hand[i].number]++;
}

hand[i].suit と hand[i].number には、手札の第 i 枚目のカードのマークと数字を表す整数が格納されています。

ですので、 count_suit[hand[i].suit]++ と count_number[hand[i].number]++ を行えば、そのマークと数字のカードの枚数をカウントアップすることができます。

さらに、これを手札のカード5枚全てに対して行うことで、手札にある各マーク・各数字のカードの枚数を数えることができます。

さらに、その結果は count_suit と count_number に格納されますので、例えばマーク a のカードの枚数は count_suit[a] で、数字 b のカードの枚数は count_number[b] により取得することができるようになります。

集計結果から役を判断して表示する

集計が終わった後は、その集計結果を参照しながら役を判断していきます。

特にワンペア・ツーペア・スリーカード・フルハウス・フォーカードあたりの役は、この集計結果を参照するだけで簡単に役の判断を行うことが可能です。

例えばフルハウスの場合、手札のカードの中に3枚の同じ数字のカードと2枚の同じ数字のカードがある場合にフルハウスであると判断することができます。

この判断は、count_number の各要素の中に 3 と 2 のものが両方存在するかどうかを調べるだけで実現することができます。

したがって、手札のカードがフルハウスであるかどうかの判断は、下記のような関数で行うことができます。手札のカードの役がフルハウスの場合は 1 を、そうでない場合は 0 を返却する関数になります。

フルハウスかどうかの判断
int isFullHouse(void) {
    int i;
    int three_card;
    int two_card;

    three_card = 0;
    two_card = 0;
    for (i = 1; i < 14; i++) {

        /* 数字がiのカードの枚数が2のものを探す */
        if (count_number[i] == 2) {
            two_card++;
        }

        /* 数字がiのカードの枚数が3のものを探す */
        if (count_number[i] == 3) {
            three_card++;
        }
    }

    if (two_card == 1 && three_card ==1) {
        /* 両方あればフルハウス */
        return 1;
    }

    return 0;

}

同様に、手札のカードがフォーカードであるかどうかの判断は、count_number の各要素の中に 4 のものが存在するかどうかを調べるだけで実現することができますし、ツーペアであるかどうかの判断は、count_number の各要素の中に 2 のものが2つ存在するかどうかを調べるだけで実現することができます。

また、手札のカードがフラッシュを満たすかどうかは、count_suit の各要素の中に 5 のものが存在するかどうかを調べるだけで判断することが可能です。

フラッシュを満たすかどうかの判断
int isFlush(void) {
    int i;

    for (i = 0; i < 4; i++) {
        /* 同じマークのカードが5枚のものを探す */
        if (count_suit[i] == 5) {
            /* あればフラッシュ */
            return 1;
        }
    }

    return 0;
}

こんな感じで、count_number や count_suit の各要素の値を調べるだけで判断できる役がたくさんあるので、事前に各カードと各数字のカードの枚数を集計しておけば、役の判断も簡単に行うことができます。

また、ストレートに関しては、連続する5つの数字全てに対して count_number の値が 1 であるかどうかを調べることで、役を満たしているかどうかの判断を行うことができます。

ストレートを満たすかどうかの判断
int isStraight(void) {
    int i, j;

    for (j = 1; j <= 14 - 5; j++) {
        for (i = j; i < j + 5; i++) {
            if (count_number[i] != 1) {
                /* 連続する5つの数字のカードの枚数が1でなければストレートでない */
                break;
            }
        }
        if (i == j + 5) {
            /* 連続する5つの数字のカードの枚数が1の場合はストレート */
            return 1;
        }
    }

    return 0;
}

集計を事前に行わなかった場合、ストレートを満たすかどうかを判断するためには下記のような処理が必要になります。

  • 手札のカードの中から1番小さい数字のカードを探す
  • 手札のカードの中から2番目に小さい数字のカードを探す
  • 上記2つのカードの数字の差が1であるかどうかを判断する
  • 手札のカードの中から3番目に小さい数字のカードを探す
  • ・・・・ 略 ・・・・

要は、手札のカードの並びがバラバラなので、1番小さい数字のカードや2番目に小さい数字のカード等を探す処理が必要になり、処理が複雑になります。

事前に各数字のカードの枚数を集計しておけば、手札のカードの並びに関係なく、上記の isStraight 関数のように連続する5つの数字のカードの枚数が1枚であるかどうかを判断すれば良いだけになるので、役の判断が楽になります。

同様にして、他の役に関しても手札のカードの集計結果を利用して判断することが可能です。ここで紹介しなかった “役を判断する関数” に関しては、次の章のポーカーのサンプルプログラムで紹介するソースコードに載せていますのでそちらをご参照いただければと思います。

さらに役の判断後に、決定した役名を printf 等で表示してやれば、ユーザーにポーカーの結果を伝えることができます。

例えば、上記の isFullHouse が 1 を返却した場合、手札のカードの役がフルハウスであることが確定しますので、この際に printf で役名がフルハウスであることを表示してやれば良いです。

役名の表示
if (isFullHouse()) {
    printf("[あなたの手札の役]\n  フルハウス\n");
}

ここで1点補足しておくと、上記にも載せた isFlush と isStraight 関数は、正確には手札の役を判断するのではなく、手札がフラッシュ / ストレートの条件を満たすかどうかを判断する関数になります。

満たすかどうかを判断しているだけなので、例えば isFlush は手札のカードの役がフラッシュの時だけでなく、ロイヤルストレートフラッシュとストレートフラッシュの場合も 1 を返却するようになっています。

逆に考えると、ロイヤルストレートフラッシュでもストレートフラッシュでもない手札のカードに対して isFlush が 1 を返却した場合、手札のカードの役はフラッシュであることが確定します。

これを利用して、次のポーカーのサンプルプログラムで紹介するソースコードの judge 関数においては、強い役から役を判断するようにしています。

これにより、手札のカードの役がロイヤルストレートフラッシュやストレートフラッシュでない時のみ、judge 関数から isFlush が実行されるようになります(手札のカードの役はロイヤルストレートフラッシュでもストレートフラッシュでもないので、isFlush が 1 を返却した時は手札のカードの役がフラッシュであることが判断できる)。

ポーカーのサンプルプログラム

最後に私が作成したポーカーのサンプルプログラムを紹介していきます。

ソースコード

ここまでの解説内容を踏まえて作成したポーカーのサンプルプログラムのソースコードは下記のようになります。

ポーカー
#include <stdio.h>
#include <stdlib.h> /* rand/srand */
#include <time.h> /* time */
#include <string.h> /* strcmp */

#define NUM_SUIT 4 /* マークの種類(4以下を設定) */
#define NUM_NUMBER 13 /* 数字の種類(13以下を設定) */
#define NUM_CARD (NUM_SUIT * NUM_NUMBER) /* 5以上である必要あり */
#define NUM_HAND 5 /* 手札の数 */
#define NUM_EXCHANGE 1 /* 交換を行うループの回数 */
#define TRUE 1
#define FALSE 0

/* カードを表す構造体 */
typedef struct _CARD {
    int suit; /* マークを表す数値(0 - 3) */
    int number; /* 数字を表す数値(1 - 13) */
} CARD;

static CARD cards[NUM_CARD]; /* カード */
static CARD hand[NUM_HAND]; /* 手札のカード */
static int top; /* 配っていないカードの中で一番上のカードの位置 */
static int count_suit[NUM_SUIT]; /* 手札の中の各マークの枚数 */
static int count_number[NUM_NUMBER + 1]; /* 手札の中の各数字の枚数 */

/* マークを表す文字列の配列 */
static const char *suit_str[4] = {
    "D", "H", "S", "C"
};


/* 数字を表す文字列の配列 */
static const char *number_str[14] = {
    "0", "A", "2", "3", "4", "5", "6",
    "7", "8", "9", "10", "J", "Q", "K"
};

void init(void) {
    int i, j;

    /* 乱数の初期化 */
    srand((unsigned int)time(NULL));

    /* カードの初期化 */
    for (j = 0; j < NUM_SUIT; j++) {
        for (i = 0; i < NUM_NUMBER; i++) {

            /* マークを設定 */
            cards[j * NUM_NUMBER + i].suit = j;

            /* 数字を設定 */
            cards[j * NUM_NUMBER + i].number = i + 1;
        }
    }

    /* 最初に配るカードを1番上のカードに設定 */
    top = 0;
}

void shuffle(void) {
    unsigned int i, j;
    CARD tmp;

    /* シャッフル範囲の末尾を設定 */
    i = NUM_CARD - 1;

    while (i > 0) {
        /* シャッフル範囲(0〜i)からランダムに1つデータを選択 */
        j = rand() % (i + 1);

        /* ランダムに決めたデータとシャッフル範囲の末尾のデータを交換 */
        tmp = cards[j];
        cards[j] = cards[i];
        cards[i] = tmp;

        /* シャッフル範囲を狭める */
        i--;
    } 
}

void deal(void) {
    int i;

    /* cardsの先頭のNUM_HAND枚を手札とする */
    for (i = 0; i < NUM_HAND; i++) {

        /* 配られたカードを手札のカードとしてコピー */
        hand[i] = cards[top];

        /* 次に配るカードを新たな1番上のカードに設定 */
        top++;
    }
}

void show(void) {
    int i;

    printf("[あなたの手札]\n");
    printf("  ");
    for (i = 0; i < NUM_HAND; i++) {
        printf("%s:%s  ", suit_str[hand[i].suit], number_str[hand[i].number]);
    }
    printf("\n\n");

}

void exchange(void) {
    char input[256];
    int exchange[NUM_HAND];
    int i;

    printf("[カードの交換]\n");

    for (i = 0; i < NUM_HAND; i++) {
        /* y/nのどちらかが入力されるまでループ */
        while (1) {

            /* カードを交換するかどうかの入力を受け付ける */
            printf("  %d枚目のカード(%s:%s)を交換しますか?(y/n)",
                i + 1, suit_str[hand[i].suit], number_str[hand[i].number]);
            scanf("%s", input);

            if (strcmp(input, "y") == 0) {
                /* i枚目を交換することを覚えておく */
                exchange[i] = TRUE;
                break;
            } else if (strcmp(input, "n") == 0) {
                /* i枚目を交換しないことを覚えておく */
                exchange[i] = FALSE;
                break;
            }
            printf("  yもしくはnを入力してください!!!\n");
        }
    }

    for (i = 0; i < NUM_HAND; i++) {
        if (exchange[i] == TRUE) {
            /* 交換を支持されたカードの場合 */

            if (top >= NUM_CARD) {
                printf("  カードが足りなくて%d枚目のカードは交換できませんでした...\n", i + 1);
                continue;
            }

            /* 一番上のカードを取って手札のカードと入れ替える */
            hand[i] = cards[top];
            top++;
        }
    }

    printf("\n");
}

int isFlush(void) {
    int i;

    for (i = 0; i < NUM_SUIT; i++) {
        /* 同じマークのカードが5枚のものを探す */
        if (count_suit[i] == NUM_HAND) {
            /* あればフラッシュ */
            return TRUE;
        }
    }

    return FALSE;
}

int isStraight(void) {
    int i, j;

    for (j = 1; j <= NUM_NUMBER + 1 - NUM_HAND; j++) {
        for (i = j; i < j + NUM_HAND; i++) {
            if (count_number[i] != 1) {
                /* 連続する5つの数字のカードの枚数が1でなければストレートでない */
                break;
            }
        }
        if (i == j + NUM_HAND) {
            /* 連続する5つの数字のカードの枚数が1の場合はストレート */
            return TRUE;
        }
    }

    return FALSE;
}

int isRoyalStraightFlush(void) {
    int i;

    if (!isFlush()) {
        /* フラッシュでなければロイヤルストレートフラッシュではない */
        return FALSE;
    }

    if (count_number[1] != 1) {
        /* 数字が1の枚数が1でなければロイヤルストレートフラッシュではない */
        return FALSE;
    }

    for (i = 10; i <= 13; i++) {
        if (count_number[i] != 1) {
            /* 数字が10-13の枚数が1でなければロイヤルストレートフラッシュではない */
            return FALSE;
        }
    }

    return TRUE;
}

int isStraightFlush(void) {

    if (isFlush() && isStraight()) {
        /* フラッシュかつストレートならストレートフラッシュ */
        return TRUE;
    }

    return FALSE;
}

int isFullHouse(void) {
    int i;
    int three_card;
    int two_card;

    three_card = 0;
    two_card = 0;
    for (i = 1; i < NUM_NUMBER + 1; i++) {

        /* 数字がiのカードの枚数が2のものを探す */
        if (count_number[i] == 2) {
            two_card++;
        }

        /* 数字がiのカードの枚数が3のものを探す */
        if (count_number[i] == 3) {
            three_card++;
        }
    }

    if (two_card == 1 && three_card ==1) {
        /* 両方あればフルハウス */
        return TRUE;
    }

    return FALSE;

}

int isFourCard(void) {
    int i;

    for (i = 1; i < NUM_NUMBER + 1; i++) {

        /* 数字がiのカードの枚数が4のものを探す */
        if (count_number[i] == 4) {
            /* あればフォーカード */
            return TRUE;
        }
    }

    return FALSE;
}

int isThreeCard(void) {
    int i;
    int three_card;
    int two_card;

    three_card = 0;
    two_card = 0;
    for (i = 1; i < NUM_NUMBER + 1; i++) {

        /* 数字がiのカードの枚数が2のものを探す */
        if (count_number[i] == 2) {
            two_card++;
        }

        /* 数字がiのカードの枚数が3のものを探す */
        if (count_number[i] == 3) {
            three_card++;
        }
    }

    if (two_card == 0 && three_card ==1) {
        /* 2枚の組みが無い&3舞の組みがあるならスリーカード */
        return TRUE;
    }

    return FALSE;
}

int isTwoPair(void) {
    int i;
    int num_pair;

    num_pair = 0;
    for (i = 1; i < NUM_NUMBER + 1; i++) {
        /* 数字がiのカードの枚数が2のものを探す */
        if (count_number[i] == 2) {
            /* ペア数をカウントアップ */
            num_pair++;
        }
    }

    if (num_pair == 2) {
        /* 同じ数字のカードの組が2つならツーペア */
        return TRUE;
    }

    return FALSE;
}

int isOnePair(void) {
    int i;
    int num_pair;

    num_pair = 0;
    for (i = 1; i < NUM_NUMBER + 1; i++) {
        /* 数字がiのカードの枚数が2のものを探す */
        if (count_number[i] == 2) {
            /* ペア数をカウントアップ */
            num_pair++;
        }
    }

    if (num_pair == 1) {
        /* 同じ数字のカードの組が1つならワンペア */
        return TRUE;
    }

    return FALSE;
}

void count(void) {
    int i;

    /* 各々のマークの枚数を0に初期化 */
    for (i = 0; i < NUM_SUIT; i++) {
        count_suit[i] = 0;
    }

    /* 各々の数字の枚数を0に初期化 */
    for (i = 1; i < NUM_NUMBER + 1; i++) {
        count_number[i] = 0;
    }

    /* 各々のマークと数字の枚数をカウント */
    for (i = 0; i < NUM_HAND; i++) {
        count_suit[hand[i].suit]++;
        count_number[hand[i].number]++;
    }
}

void judge(void) {

    /* 手札の各カードのマークと数字をカウント */
    count();

    if (isRoyalStraightFlush()) {
        printf("[あなたの手札の役]\n  ロイヤルストレートフラッシュ\n");
        return;
    }

    if (isStraightFlush()) {
        printf("[あなたの手札の役]\n  ストレートフラッシュ\n");
        return;
    }

    if (isFourCard()) {
        printf("[あなたの手札の役]\n  フォーカード\n");
        return;
    }

    if (isFullHouse()) {
        printf("[あなたの手札の役]\n  フルハウス\n");
        return;
    }

    if (isFlush()) {
        printf("[あなたの手札の役]\n  フラッシュ\n");
        return;
    }

    if (isStraight()) {
        printf("[あなたの手札の役]\n  ストレート\n");
        return;
    }

    if (isThreeCard()) {
        printf("[あなたの手札の役]\n  スリーカード\n");
        return;
    }

    if (isTwoPair()) {
        printf("[あなたの手札の役]\n  ツーペア\n");
        return;
    }

    if (isOnePair()) {
        printf("[あなたの手札の役]\n  ワンペア\n");
        return;
    }

    /* どの役でもなかった */
    printf("[あなたの手札の役]\n  ノーペア\n");
}

int main(void) {

    int i;

    /* ゲームの初期化 */
    init();

    /* カードをシャッフル */
    shuffle();

    /* カードを配る */
    deal();

    /* 手札のカードを表示 */
    show();

    for (i = 0; i < NUM_EXCHANGE; i++) {

        /* 手札のカードを交換 */
        exchange();

        /* 手札のカードを表示 */
        show();
    }

    /* 役を判断して表示 */
    judge();
    
    return 0;
}

ちょっとグローバル変数に頼りすぎていますので、本当はグローバル変数もローカル変数として宣言して各関数に引数として渡した方が良いです…。

スポンサーリンク

ソースコードの解説

基本的にはポーカーの作り方で紹介したソースコードを切り貼りして作成したものになります。

ただし、カードの枚数を表す 52、マークの種類を表す 4、数字の種類を表す 13、手札のカードの枚数を表す 5 などに関しては、即値ではなく define によって定義した定数名に置き換えています。

定数の定義
#define NUM_SUIT 4 /* マークの種類(4以下を設定) */
#define NUM_NUMBER 13 /* 数字の種類(13以下を設定) */
#define NUM_CARD (NUM_SUIT * NUM_NUMBER) /* カードの枚数(5以上) */
#define NUM_HAND 5 /* 手札のカードの枚数数(5固定) */
#define NUM_EXCHANGE 1 /* 交換を行うループの回数 */
#define TRUE 1
#define FALSE 0

main 関数から呼び出している各関数で行っていることと、ポーカーの作り方で解説した内容との対応づけを下記にまとめておきますので、ソースコードを読むときの参考にしてください。

init 関数

init 関数では、ポーカーを開始するにあたって必要な初期化処理を行なっています。

カードへのマークと数字の割り当てで解説した配列 cards の各要素へのマークと数字の割り当てや、rand 関数を利用するための srand 関数によるシードの設定、まだ配っていないカードの一番上の位置を示す top 変数の初期化を行ったりしています。

ちなみに、配列 cards はグローバル変数として宣言しています。

shuffle 関数

shuffle 関数ではカードの交換を繰り返すことでカードのシャッフルを行なっています。

この関数で行なっている処理については、カードを配るのカードをランダムにシャッフルするで解説しています。

deal 関数

deal 関数では、プレイヤーにカードを5枚配る処理を行なっています。

この関数で行なっている処理については、カードを配るの一番上のカードから配るで解説しています。

show 関数

show 関数では、プレイヤーの手札の5枚のカードの表示を行なっています。

この関数で行なっている処理についてはカードを表示するで解説しています。

exchange 関数

exchange 関数では、手札のカードの交換を行う処理を行なっています。

この関数で行なっている処理についてはカードを交換するで解説しています。

judge 関数

judge 関数では、手札のカードの役を判断し、その役名を表示する処理処理を行なっています。

この関数で行なっている処理については役を判断して表示するで解説しています。

プログラムの設定

上記でも示した定数の定義値を変更することで、多少のプログラムの設定を行えるようにしています。

例えば、NUM_SUIT を 1 にすれば配られるカードが全てダイヤになりますので、必ずフラッシュの役を作ることができるようになります。

さらに、NUM_NUMBER を小さくすれば、スリーカードやツーペアなどの役を作りやすくなります。

ただし、カードの枚数 NUM_SUIT * NUM_NUMBER が 5 未満になるとポーカーがプレイできないので注意してください。

また、NUM_EXCHANGE を 2 以上にすれば、カードの交換処理をその値分繰り返し実行することができるようになります。交換回数が多くなるので、強い役も作りやすくなると思います。

この辺りを利用すれば役が作りやすくなるので、自身で役の判断処理を実装した時などの動作確認に活用することができると思います。 

まとめ

このページでは、C言語での「ポーカーの作り方」について解説しました!

ポーカーを作ってみた私の感想としては、ポーカーを作る上でポイントになるのは下記の2点だと思います。

  • カードの配り方
  • 役の判断の仕方

前者に関しては、特にランダムに、かつ重複なしにカードを配るところがポイントだと思います。これは、事前にカードをランダムにシャッフルしておくことで簡単に実現することができます。

また後者に関しては、いかにシンプルに役を判断するかというところがポイントだと思います。単純に手札のカードのマークと数字を調べながら判断を行うと処理が大変なので、事前に各マークと各数字のカードの数を集計しておくことをオススメします。

出来上がりとしては単純にコンソールに文字を表示するだけのものになりますが、それでも実際に作ってみると充実感もありますし学べることも多いです!

是非このページを参考にして、他のカードゲームの作成にも挑戦してみてください!

また、このサイトではオセロの作り方に関しても解説していますので、C言語でオセロを作ってみたい方は是非下記ページも読んでみていただければと思います。

オセロゲームのC言語ソースコード紹介ページのアイキャッチ C言語でオセロゲームを作成

同じカテゴリのページ一覧を表示