【C言語】ババ抜きの作り方

C言語でのババ抜きの作り方解説ページアイキャッチ

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

このページでは、C言語での「ババ抜き」の作り方について解説していきます!

ババ抜きに関しては皆さんもプレイしたことがあるのではないでしょうか?

特にプログラミング初心者の方には「プレイしたことのある簡単なゲームをプログラミングして作ってみる」ことは非常にオススメです。プレイしたことのあるゲームであれば目標となるプログラムが明確ですし、実際にプログラミングすることで多くの気づきも得られ、更にいろんな処理の実現方法を考えていくことで今後プログラミングで使えるテクニック等も学ぶことができます。

もちろんゲームなので作っていて楽しいと思いますし、出来上がった際の充実感も大きいと思います。

今回は「ババ抜き」を題材にして作り方を解説していきますが、このページの解説内容を参考にして他のゲームの開発にも是非挑戦してみてください!

それでは、ババ抜きの作り方について解説していきたいと思います!最初は「ババ抜き」の流れのおさらいから説明していくことになりますので、すぐにプログラムを知りたいという方は 「ババ抜き」のプログラム までスキップしていただければと思います。

「ババ抜き」の流れ

では、まずはババ抜きのゲームの流れをおさらいしていきたいと思います。

自身がプレイしたことのあるゲームをプログラミングで開発する際には、まずはそのゲームで行われる動作の流れを考えてみるのが良いと思います。

そして、その流れの中で行われることを処理として実装し、さらにそれらを適切なタイミングで実行するようにすればゲームとして完成します。

ということで、まずはババ抜きのゲームとしての流れをおさらいしていきましょう!

まずババ抜きではカードがシャッフルされ、さらに各プレイヤーに対してカードが配られます。

カードが配られた後は、各プレイヤーが手札を確認してペアとなるカードを見つけ出し、その見つけたペアのカードを捨てます。

ここまでが前準備のようなもので、ここからはプレイヤーが隣のプレイヤーからカードを引き、引いたカードがペアになったのであればそのペアのカードを捨てます。ただし、カードを引く際には、事前にどのカードを引くのか?を決定しておく必要があります。

これらをプレイヤーの順番を回しながら繰り返していけば、次第にプレイヤーのカードが減っていくことになります。手札のカードがなくなったらそのプレイヤーは「勝ち」となります。そして、手札の残っているプレイヤーが一人になった際に勝負が決着し、最後まで手札にカードが残っていたプレイヤーが「負け」となります。

以上がババ抜きのゲームの流れであり、これらの流れを箇条書き形式で書き出せば下記のようになります。

  1. カードをシャッフルして配る
  2. 手札の中からペアになっているカードを捨てる
  3. 「カードを引く」&「カードを引かれる」プレイヤーを決める
  4. 引くカードを決める
  5. カードを引く
  6. 引いたカードがペアになればカードを捨てる
  7. 「ゲーム終了」かどうかを判断する
  8. ゲーム終了でなければ 3. に戻る

要は、これらの各処理を関数などで実現し、後は main 関数等から上記の流れが実現できるように各関数を呼び出してやればババ抜きが完成することになります。

「ババ抜き」に必要な処理の実現

ということで、先ほど挙げた各処理を関数として実現していきたいと思います。

が、処理を実現する前に、まずはババ抜きを実現する上での「データの扱い」について解説します。ババ抜きのプログラムではカードを扱いますし、さらにはプレイヤーを扱う必要もあります。これらをC言語で扱っていくための考え方について説明していきます。

そして、その後に、先ほど挙げた各処理の実現方法について解説していきたいと思います。

スポンサーリンク

トランプのカードを扱う

まず、ババ抜きはトランプゲームであり、トランプのカードを扱うゲームになります。

「何を当たり前のことをわざわざ…」と思われるかもしれませんが、ここは結構大事で、特にポイントになるのは「トランプのカードをどうやってプログラムの中で扱うか?」という点になります。

例えば、カードではなく単なる整数を扱うゲームなのであれば、扱う整数を int 型の変数などで管理すれば良いだけになります。

ですが、ババ抜きで扱われるのはトランプのカードです。C言語では数値や文字等を管理するための型は存在しますが、カードを扱うための型は存在しません。

カードを扱う型の定義

ですが、C言語では自身で新たな型を定義し、それで数値や文字以外のデータを扱うことが可能です。

具体的には、構造体や typedef を利用することで「新たな型」を定義することが可能となります。構造体や typedef については下記ページで解説していますので、詳しく知りたいかたは下記ページをご参照いただければと思います。

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

今回は、構造体と typedef を利用してカードを扱うための型である CARD を定義していきたいと思います。ババ抜きにおいて、トランプのカードで重要になるのはマークと数字になります。そのため、下記のようにマークを扱うメンバ suit と数字を扱うメンバ number を持つ型を CARD 型として定義していきます。

CARD型の定義
/* カードを表す構造体 */
typedef struct _CARD {
    int suit; /* マークを表す整数 */
    int number; /* 数字を表す整数 */
} CARD;

この CARD 型の変数1つ or CARD 型の配列の要素1つが1枚のトランプのカードを表現することになります。

トランプのカードの管理方法の説明図

53枚のカードの用意

さらに、ババ抜きで使用するカード全てが扱えるよう、カードの枚数(NUM_CARDS)をサイズとする CARD 型の配列 cards をグローバル変数として宣言しておきます。

念の為補足しておくと、ババ抜きで使用するトランプのカードは計53枚なので NUM_CARDS53 となります(113 までの数字のカードが4種類ずつ存在し、さらにジョーカーのカードが存在する)。

cardsの定義
static CARD cards[NUM_CARD]; /* カードの配列 */

この cards において、各要素が1枚のカードを表す CARD 型の要素となります。ただし、宣言しただけではマーク(suit)と数字(number)が各要素に設定されていないため、ゲームを開始する前に予めマークと数字を設定しておく必要があります。

このマークと数字の設定は、下記のような処理により実現することができます(NUM_SUIT はマークの種類数 4NUM_NUMBER は数字の種類数 13 を表す定数マクロになります)。

カードの情報の設定
/* カードの初期化 */
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;
    }
}

/* 最後の一枚はジョーカーに設定 */
cards[NUM_CARD - 1].suit = 4;
cards[NUM_CARD - 1].number = 14;

ループの後に設定している要素 cards[NUM_CARD - 1] がジョーカーを表すカードとなり、ジョーカーであることが分かるように、他のマークや他の数字とは異なる値である 4suit に、さらに 14number に設定するようにしています。

上記のように処理を行えば、cards の各要素をトランプのカードとして扱うことができるようになります。

カードを文字列で表現

ただし、配列 cards の各要素のメンバに設定されているのは単なる整数になります。カードの番号を表示する際には整数でも良いですが、マークを表示する際にそのまま整数が表示されてもユーザーにはどの種類のマークであるかが分かりません。

カードを文字列として表示するため、今回は下記のような配列を用意しておきたいと思います。このような配列を用意しておけば、各配列の添字に CARD 型の変数や要素のメンバを指定することで、それを文字列に変換した結果を取得することができます。

カードを文字列として扱うための配列
/* マークを表す文字列の配列 */
static const char *suit_str[NUM_SUIT + 1] = {
    "D", "H", "S", "C", "B"
};


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

例えば下記のように printf の引数として上記の配列を使用することで、cards[c1] の要素を文字列として表示することができます。

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

例えば cards[c1].suit = 2cards[c1].number = 13 の場合は、上記によりスペードのキングを表す S:K が表示されることになります。また、カードがジョーカーの場合はババであることがわかりやすいように B:B が取得されるようにしています。

今回の例のように、プログラムの中では整数として扱っている場合でも、ユーザーに向けて表示を行う際には文字列に変換したいというケースは結構多いです。そして、このような場合は上記のような配列から文字列への変換結果を取得するテクニックが使えますので、この考え方は覚えておくと良いと思います。

複数人のプレイヤーを扱う

カードが扱えるようになったため、次はプレイヤーを扱えるようにしていきたいと思います。

プレイヤーを扱う型の定義

ババ抜きにおける各プレイヤーは手札にカードを持っており、各プレイヤーが他のプレイヤーからカードを引いたり、ペアになったカードを捨てたりするなどして、手札のカードを変化させながら進行していくゲームとなります。

実際にババ抜きをプレイをしている際は、最低限自身の手札のカードのみを管理していけば良いのですが、ババ抜きをプログラミングで開発する場合、全てのプレイヤーの手札のカードを管理する必要があります。

こういった各プレイヤーの手札のカードを管理していけるように、まず、下記のように PLAYER 型を定義したいと思います。

PLAYER型の定義
/* プレイヤー表す構造体 */
typedef struct _PLAYER {
    CARD hand[NUM_CARD]; /* 手札のカードの配列 */
    int num_hand; /* 手札のカードの枚数 */
} PLAYER;

PLAYER 型はプレイヤーの手札のカードを管理する hand と手札のカードの枚数を管理する num_hand の2つのメンバから構成される型となります。

プレイヤーの管理方法の説明図

handCARD 型の配列であり、hand のサイズである NUM_CARD は前述の通りカードの枚数である 53 を示す定数マクロになります。プレイヤーの人数が一人の場合は最初に 53 枚のカードが配られることになるため、一応この最悪値を見越して hand の要素数を設定しています。

このように、手札のカードとなる最悪値を配列 hand のサイズとしていますが、実際にはプレイヤーの手札のカード枚数は num_hand となります。したがって、例えば PLAYER 型の変数 player の手札のカードは player.hand[0]player.hand[player.num_hand - 1] のみとなります。

プレイヤーの手札のカードの管理に対する説明図

複数のプレイヤーの用意

さらに、下記のように PLAYER 型の配列 players をグローバル変数として宣言することで、複数人のプレイヤーを管理できるようにしていきたいと思います。ここで NUM_PLAYER はプレイヤーの人数を表す定数マクロとなります。

playersの定義
static PLAYER players[NUM_PLAYER]; /* プレイヤーの配列 */

この配列 players の型は PLAYER なので、players の各要素はメンバとして handnum_hand を持っており、これらによって各プレイヤーの手札のカードとその枚数を管理していくことができます。例えば p 番目のプレイヤー(0 ≦ p < NUM_PLAYER) の手札は配列 players[p].hand で管理し、p 番目のプレイヤーの手札のカードの枚数は players[p].num_hand で管理します。

また、実際にババ抜きをプレイする場合はプレイヤーは全て「人」になるのですが、プログラミングで開発するゲームの場合はプレイヤーを人だけでなくコンピューターとする場合も多いです。今回は人がプレイヤーとして参加可能な人数は最大1人とし、その他のプレイヤーはコンピューターとしたいと思います。

カードをシャッフルして配る

ここまでは主にデータの扱い方に対する説明になります。

ここからは、ババ抜きを実現するために必要な処理の実現方法について解説していきます。解説していく順番が ババ抜き」の流れ で示した流れの順番とは前後するので、その点はご了承ください。

カードを配る

まず、ババ抜きはトランプゲームの1つであり、トランプのカードがプレイヤーに配られることでスタートします。

前述の通り、ババ抜きで扱う53枚のカードの情報は配列 cards の各要素に設定されていることになります。さらに、プレイヤーの手札は配列 players のメンバである配列 hand で管理を行うようにしています。

したがって、カードを配る処理は、配列 cards の各要素を配列 playersのメンバである配列 hand の最後尾の1つ後ろに追加していくことで実現することができます。また、各プレイヤーに均等な枚数が配られるよう、1枚ずつ配り先のプレイヤーを変更していく必要があります。

カードを各プレイヤーに配る様子

このような「トランプのカードを各プレイヤーに配る」ような関数は、下記のように処理を実装することで実現することができます。

カードを配る
/* カードを各プレイヤーに配る */
void deal(void) {
    int i;
    int top = 0;
    int p = 0;

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

        /* カードの配り先となるプレイヤーを決定 */
        p = i % NUM_PLAYER;

        /* 配られたカードを手札の一番後ろにコピー */
        players[p].hand[players[p].num_hand] = cards[top];

        /* プレイヤーの手持ちカード枚数を1増やす */
        players[p].num_hand++;

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

上記の for ループの内側では、p 番目のプレイヤーに対してカードを1枚配る処理を行なっています。pi % NUM_PLAYER で求めているため、繰り返しのたびに次のプレイヤーに対してカードが配られるようになっています。

もう少し具体的に説明すると、players[p].num_handp 番目のプレイヤーにおける手札のカードの枚数であるため、players[p].hand[players[p].num_hand]p 番目のプレイヤーにおける手札のカードの最後尾の1つ後ろの位置の要素となります。そこに cards の要素をコピーすることで、カードを配る処理を実現しています。また、カードを受け取ったプレイヤーの手札のカードの枚数は 1 増えることになるので、それに合わせて players[p].num_hand++ を実行するようにしています。

あとは上記の処理を cards の先頭の要素から順に NUM_CARD 回分繰り返せば、全カードがプレイヤーに配られることになります。

カードをシャッフルする

ただし、上記の deal 関数では、トランプのカードを扱う で用意した配列 cards の先頭から順にプレイヤーにカードを配るようになっているため、単に deal 関数を実行するだけだと毎回同じカードが同じプレイヤーに配られることになってしまいます。プレイヤーに配られるカードにランダム性を持たせるためには、事前にカードをシャッフルしてから配る方が良いです。

このような「カードをシャッフルする」処理は下記のように実装することで実現することができます。

カードをシャッフルする
/* カード(cards配列)をシャッフルする */
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--;
    } 
}

上記の shuffle 関数の中で利用している rand は乱数を生成する関数になります。詳細に関しては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。

事前に srand 関数を実行しておく必要がある点がポイントになると思います。

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

また、上記の shuffle 関数の中では Fisher–Yates shuffle というアルゴリズムに基づいてカードのシャッフルを行なっています。この Fisher–Yates shuffle は「重複無し」の乱数を生成する際に便利な考え方なので、特にゲームなどをプログラミングしたいと考えている方は覚えておくと良いと思います。下記ページで詳細を解説していますので、興味のある方は是非読んでみてください。

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

スポンサーリンク

手札の中からペアになっているカードを捨てる

カードが配られた後は各プレイヤーが手札の中でペアになっているカードを捨てていくことになります。ペアになっているカードとは「同じ番号の2枚のカード」になります。

このペアになっているカードを捨てる処理は下記ように2つの処理を段階的に実行して実現する必要があります。

  • ペアになっているカードを探す
  • ペアになっているカードを捨てる

つまり、まず手札の中からペアのカードを全て探し出し、見つかったペアのカードを捨てるような処理を行う必要があります。そして、これらの処理を全プレイヤーに対して実行する必要があります。

ペアになっているカードを探す

まずは特定のプレイヤーの手札の中から指定された1枚のカードと「ペアになっているカードを探す」処理を実現する方法について考えていきましょう。

このような処理は、その特定のプレイヤーの手札の中から “指定されたカードと同じ番号のカード” を探索することで実現することができます。

ババ抜きにおけるペアの説明図

指定されたカード以外に同じ番号を持つカードが手札の中に存在する場合、指定されたカードと見つけたカードはペアとなるため、これら2つのカードに対して次に説明する「ペアになっているカードを捨てる」処理を行います。

ペアになったカードに対して捨てる処理を行うことを説明する図

指定されたカード以外に同じ番号を持つカードが手札の中に存在しない場合は、指定されたカードとペアになるカードが存在しないことになりますので「ペアになっているカードを捨てる」処理はスキップすることになります。

このような考え方に基づき、引数で与えられた p 番目のプレイヤーの手札の中から「c 番目の位置に存在するカードとペアになるカード」を探索する関数は下記のように実装して実現することができます(p0プレイヤーの人数 - 1c0p 番目のプレイヤーの手札のカード枚数 - 1 の範囲の整数となります)。

ペアを探す
/* プレイヤーpの手札のc枚目のカードと同じ番号のカードを取得する */
int getPair(int p, int c) {
    int i;

    for (i = 0; i < players[p].num_hand; i++) {
        if (i != c) {
            if (players[p].hand[i].number == players[p].hand[c].number) {
                /* i枚目のカードとc枚目のカードとは同じ番号なのでiを返却 */
                return i;
            }
        }
    }

    /* c枚目のカードと同じ番号のカードは手札に存在しないので-1を返却 */
    return -1;
}

上記の getPair 関数では p 番目のプレイヤー players[p] の手札の c 枚目のカードである players[p].hand[[c]] の番号(number メンバ)と同じ番号を持つカードを、players[p] の手札である配列 players[p].hand の中から探索を行なっています。

getPair関数の説明図

同じ番号のカードが見つかった場合は、そのカードの位置(players[p].hand の添字)を返却し、見つからなかった場合は -1 を返却するようにしています。

今回は単純な方法で探索を行なっていますが、探索アルゴリズムを改善することで探索の効率化を図るようなことも可能です。この辺りは下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

探索アルゴリズム解説ページのアイキャッチ 【C言語】データの探索アルゴリズム(線形探索・二分探索)について解説

1枚のカードを捨てる

続いてペアになっているカードを捨てる処理の実現方法について考えていきたいと思います。

まずはペアではなく、1枚のカードを捨てる処理について考えていきましょう!

ここでは、p 番目のプレイヤーの手札から1枚のカードを捨てる処理について考えていきたいと思います。すなわち、players[p].hand の配列から捨てたいカードを削除する処理を実現していきます。

また、今回は players[p].handc 枚目のカードを捨てることを前提に解説をしていきます。c0 ≦ c < players[p].num_hand を満たす整数であり、players[p].num_handp 番目のプレイヤーの手札のカードの枚数となります。

カードを捨てる処理の実現方法はいくつか存在するのですが、今回は c 枚目のカードよりも後ろ側に存在するカードを1枚分ずつ前側に移動させることでカードを捨てる処理を実現したいと思います。これにより、c 枚目のカードは c + 1 枚目のカードで上書きされるため、c 枚目のカードはプレイヤーの手札から存在しなくなります(下の図は players[p].num_hand8 であり、さらに c=2 枚目のカードを捨てる際の処理を説明する図になります)。

カードを捨てる処理の説明図1

さらに、p 番目のプレイヤーの手札のカードを捨てるのですから、p 番目のプレイヤーの手札の枚数は 1 減ることになります。そのため、上記の上書き処理を行なった後に players[p].num_hand-- を実行します。

これにより、手札から c 枚目のカードが存在しなくなり、さらに手札のカードの枚数も 1 減ることになるため、カードを捨てる処理が実現できていることになります。

カードを捨てる処理の説明図2

このようなカードを捨てる処理は、下記の discard 関数により実現することができます。

カードを捨てる
/* p番目のプレイヤーの手札からc枚目のカードを捨てる */
void discard(int p, int c) {
    int i; 

    /* c+1枚目以降ののカードを1つ前側に移動 */
    for (i = c; i < players[p].num_hand - 1; i++) {
        players[p].hand[i] = players[p].hand[i + 1];
    }
    
    /* 手持ちのカード数を1減らす */
    players[p].num_hand--;

}

ちょっと面倒な処理になってしまいますが、これは「C言語の配列から要素の削除はできない」ことが原因です。そのため、各要素の位置をずらすような処理を行なって「カードを捨てる処理」を実現する必要があります。実は、例えば下記ページで紹介している「リスト構造」を利用すれば実際に要素の削除を行うことができ、各要素の位置をずらすような処理が不要となって「カードを捨てる処理」がもっとシンプルになります。

リスト構造の解説ページアイキャッチ 【C言語】リスト構造について分かりやすく解説【図解】

ただ、リスト構造を用意するのに手間がかかるため、今回は配列でカードを管理するようにしています。特に要素の削除や追加を行いたい場合はリスト構造が非常に便利ですので、興味があれば是非リスト構造の解説ページも読んでみてください。

また、配列から要素を削除する際に、上記のように “削除対象の後ろ側の要素を1つずつ前側に移動する” & “要素数を管理する変数の値を1つ減らす” というやり方はさまざまな場面で使えるテクニックとなるので覚えておくと良いと思います。

ペアの2枚のカードを捨てる

話が脱線してしましたが、上記の discard 関数により、p 番目のプレイヤーの手札から c 枚目のカードを削除することができるようになりました。ただ、ここで本当に行いたいのは1枚のカードを捨てることでなく、ペアになった2枚のカードを捨てることになります。ですが、上記の discard 関数は、あくまでも1枚のカードを捨てる関数です。

例えば、ペアになっているカードを探す で紹介した getPair(p, c1) の戻り値が c2 であった場合、すなわち c1 枚目のカードと c2 枚目のカードがペアであった場合、ペアとなった2枚のカードを捨てるためには、p 番目のプレイヤーの手札から c1 枚目のカードと c2 枚目のカードを捨てる必要があります。

であれば、discard(p, c1)discard(p, c2) を単に実行すれば良さそうにも感じるのですが、実はそういうわけでもないので注意してください。

なぜなら、これは 1枚のカードを捨てる でも解説したように、discard(p, c1) の実行によって c1 + 1 以降のカードが1つずつ前の位置に移動することになるからです。つまり、discard(p, c1) を実行すれば、c2c1 よりも大きい場合、c2 枚目のカードは c2 - 1 枚目のカードの位置に移動することになります。

カードを1枚捨てることにより、他のカードの位置が変わってしまう様子

したがって、c1 の位置のカードと c2 の位置のカードを捨てる際には、discard(p, c1) の実行による c2 の位置のカードの移動の有無を判断し、移動した場合は discard(p, c2 - 1) を、移動していない場合は discard(p, c2) を実行するように処理を分岐する必要があります。

c1とc2の位置関係によって2回目の実行するdiscard関数の引数を変更する必要があることを示す図

この判断は c1c2 の大小関係により実施することができ、ペアとなった2枚のカードを捨てる処理は下記の discardPair 関数により実現することができます。

ペアのカードを捨てる
/* p番目のプレイヤーの手札からc1枚目とc2枚目のカードを削除*/
void discardPair(int p, int c1, int c2) {
    int i; 

    discard(p, c1);

    if (c1 < c2) {
        discard(p, c2 - 1);
    } else {
        discard(p, c2);
    }

}

全プレイヤーの全ペアのカードを捨てる

さて、上記の discardPair 関数を利用することでペアのカードを捨てることができるようになりました。さらに、特定のカードとペアになっているカードは getPair 関数によって探索することが可能です。

続いて、これらの関数を利用して、カードが配られた後に実行する「全プレイヤーの全ペアのカードを捨てる処理」を実現していきたいと思います。おそらく、みなさんが想像した通り、ループの中でこれらの関数を実行することで、全プレイヤーの全ペアのカードを捨てる処理を実現していきます。

結論としては、下記のような allDiscard 関数により、全プレイヤーの全ペアのカードを捨てる処理を実現することができます。

全プレイヤーの全ペアを捨てる
/* 全プレイヤーの手札の中からペアになっているカードを全て捨てる */
void allDiscard(void) {
    int p, i, c1, c2;

    for (p = 0; p < NUM_PLAYER; p++) {
        c1 = 0;
        while (c1 < players[p].num_hand) {
            /* p番目のプレイヤーの手札からc1枚目のカードと同じ番号のカードを取得する */
            c2 = getPair(p, c1);
            if (c2 != -1) {
                /* 同じ番号のカードが存在した場合は、それらのカードを捨てる */
                discardPair(p, c1, c2);
            } else {
                /* 同じ番号のカードが存在しない場合は次のカードの位置をc1とする*/
                c1++;
            }
        }
    }
}

外側の for ループは各プレイヤーに対するループになっています。NUM_PLAYER はプレイヤー数を表す定数マクロであり、この for ループにより、ループの内側の処理が全プレイヤーに対して繰り返し行われることになります。

pのループで各プレイヤーに対して処理を実行する様子

さらに、内側の while ループはプレイヤーの手札の各カードに対するループになっています。このループでは、手札の各カードに c1 対し、ペアになっているカードが存在するかどうかの判断を getPair 関数で実施し、ペアが存在する場合はペアとなっているカードの2枚を discardPair で捨てる処理を行なっています。

MEMO

ペアになっているカードを探す で解説している通り、getPair 関数は引数 p と引数 c を受け取り、p 番目のプレイヤーの手札の c 枚目のカードとペアとなるカードが存在する場合に、ペアとなるカードの手札上の位置(players[p].handの添字)を返却し、ペアとなるカードが存在しない場合は -1 を返却する関数となっています

ちょっと内側の while ループが複雑になっているのは、ペアの2枚のカードを捨てる でも解説したように、カードを捨てた際に手札のカードの位置が前方に移動するようになっているからです。

c1 は手札上のカードの位置を示す変数となっており、c1 の位置のカードを捨てなかった場合は、次回のループ内の処理で「1つ後ろ側の位置のカード」に対してペアとなるカードが存在するかどうかを判断するために c1 の値を 1 増やすようにしています。

ペアが存在しなかった場合に次に注目するカードの位置を示す図

それに対し、c1 の位置のカードを捨てる場合、つまり c1 の位置のカードとペアとなるカードが存在する場合、c1 の位置のカードが捨てられ、その c1 の位置に1つ後ろ側に存在していたカード(もしくは2つ後ろに存在していたカード)が移動してくることになります。つまり、c1 を変化させなくても、次回のループの処理では自動的に元々の c1 の位置よりも後ろ側の位置のカードに対して処理が行われるようになります。

ペアが存在した場合に次に注目するカードの位置を示す図

そのため、カードを捨てるかどうか、すなわち、現在注目している c1 の位置のカードとペアとなるカードが存在するかどうかによって c1++ を実行するかどうかを切り替えるようにする必要があり、これを行うために、内側のループがちょっと複雑になっています。 

以上の処理により、カードが配られた後に実施する「ペアになっているカードを捨てる処理」が実現できたことになります。

引くカードを決める

カードが配られた後に各プレイヤーがペアになっているカードを捨てた後は、プレイヤーの順番を回しながら他のプレイヤーの手札からカードを引いていくことになります。

カードを引く際には、事前に引くカードを決めておく必要があります。ということで、次は「引くカードを決める」処理の実現方法について考えていきましょう!

まず、p1p2 とが指定された際に、p1 番目のプレイヤーが p2 番目のプレイヤーの手札から引くカードを決める処理について考えていきたいと思います。

プレイヤーが人の場合

まず p1 番目のプレイヤーが人の場合、そのプレイしている人から引きたいカードを指定して貰えば良いことになります。今回は p2 番目のプレイヤーの手札の枚数を示し、その中から引きたいカードの位置を整数で入力して指定できるようにしていきたいと思います。

プレイヤーが人の場合の引くカードの位置の決め方

この入力の受付は、皆さんもご存知の通り scanf 関数で実現することができます。ただし、入力された整数が p2 番目のプレイヤーの手札の位置として不適切である可能性もありますので、その場合は再度入力を促すようにします。

ひとまず、”p1 番目のプレイヤーが人の場合のみ” に対応した、引くカードを決める関数は下記のように実装することで実現することができます。

引くカードを決める(人)
/* p1番目のプレイヤーがp2番目のプレイヤーから引くカードを決める */
int getDrawCard(int p1, int p2) {
    int c;

    if (p1 == HUMAN) {
        /* プレイヤーが人の場合はscanfで引くカードを決める */
        do {
            printf("何枚目のカードを引きますか?:");
            scanf("%d", &c);
            /* 入力された整数が不正の場合は再度入力受付を行う */
        } while (c >= players[p2].num_hand || c < 0);

    }

    return c;
    
}

上記では、p1HUMAN という定数マクロと一致する場合に、その p1 番目のプレイヤーが人であると判断し、その場合に scanf 関数を実行して整数の入力受付を行うようにしています。

ただし、入力された値が 0players[p2].num_hand - 1 の整数でない場合は、入力された値が p2 番目のプレイヤーの手札のカードの位置として不適切であるため、再度 scanf 関数での入力受付を行うようにしています(p2 番目のプレイヤーは players[p2].num_hand 枚しか手札にカードを持っていない)。この手札のカードの位置は 0 から始まるという点に注意してください。

プレイヤーがコンピューターの場合

p1 番目のプレイヤーがコンピューターの場合は自動的にランダムに引くカードを決定してやれば良いです。ランダムに値を決定するのに便利なのが下記ページで解説している rand 関数であり、さらに rand() % players[p2].num_hand のように剰余算を実行することで 0players[p2].num_hand - 1 の整数を取得することができます。

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

p1 番目のプレイヤーがコンピューターの場合” も考慮した場合、先ほど示した getDrawCard 関数は下記のようになります。返却値が、p1 番目のプレイヤーが p2 番目のプレイヤーの手札から引くカードの位置となります。

引くカードを決める
/* p1番目のプレイヤーがp2番目のプレイヤーから引くカードを決める */
int getDrawCard(int p1, int p2) {
    int c;

    if (p1 == HUMAN) {
        /* プレイヤーが人の場合はscanfで引くカードを決める */
        do {
            printf("何枚目のカードを引きますか?:");
            scanf("%d", &c);
            /* 入力された整数が不正の場合は再度入力受付を行う */
        } while (c >= players[p2].num_hand || c < 0);

    } else {
        /* プレイヤーがコンピューターの場合はランダムに決める */
        c = rand() % players[p2].num_hand;
    }

    return c;
    
}

カードを引く

先ほど示した getDrawCard 関数により p1 番目のプレイヤーが p2 番目のプレイヤーの手札から引くカードの位置を取得することができるようになったため、次は実際に「カードを引く」処理を実現していきたいと思います。

まず、p1 番目のプレイヤーが p2 番目のプレイヤーの手札からカードを引く処理は、下記の2つの処理から実現することができます。

  • p1 番目のプレイヤーの手札にカードを加える
  • p2 番目のプレイヤーの手札からカードを削除する

p1 番目のプレイヤーの手札にカードを加える

まず、前者の p1 番目のプレイヤーの手札にカードを加える処理について考えていきましょう。

カードを引く処理の説明図1

p1 番目のプレイヤーの手札は配列 players[p1].hand で管理されているわけですから、players[p1].hand に要素として「引くカード」を加えれば良いというわけになります。今回は players[p1].hand の最後尾に要素を追加することで p1 番目のプレイヤーの手札にカードを加える処理を実現したいと思います。

この処理は、p1 番目のプレイヤーの手札の枚数が players[p1].num_hand であるため、players[p1].hand[players[p1].num_hand] に「引くカード」の情報をコピーすることで実現することができます。具体的には、「引くカード」が p2 番目のプレイヤーの手札の c の位置に存在するカードである場合、下記の処理によって p1 番目のプレイヤーの手札にカードを加えることができることになります。

カードを手札に加える
/* p2番目のプレイヤーの手札の一番後ろにc枚目のカードをp1番目のプレイヤーに追加する */
players[p1].hand[players[p1].num_hand] = players[p2].hand[c];
players[p1].num_hand++;

手札にカードを加えることで手札のカードの枚数も増えるため、players[p1].num_hand++ の実行も忘れずに行う必要があります。

p2 番目のプレイヤーの手札からカードを削除する

上記の処理によって p1 番目のプレイヤーの手札にカードが加わっため、次は p2 番目のプレイヤーの手札からそのカードを削除していきます。

削除という言葉を使うと分かりにくいかもしれませんが、要は p2 番目のプレイヤーにそのカードを1枚捨てさせれば良いことになります。

さらに、この「1枚のカードを捨てる」処理は 1枚のカードを捨てる で紹介した discard 関数で実現済みですので、結局は discard(p2, c) を実行すれば良いだけになります(cp1 番目のプレイヤーから引かれる p2 番目のプレイヤーの手札上の位置)。

カードを引く処理の説明図2

このように、p1 番目のプレイヤーの手札にカードを加え、さらに p2 番目のプレイヤーの手札からカードを削除するという考え方で「カードを引く」を実現する関数は下記のようなものになります。

カードを引く
/* p1番目のプレイヤーがp2番目のプレイヤーからカードを引く */
int draw(int p1, int p2) {
    int c;
    int i;
    
    printf("プレイヤー%dがプレイヤー%dからカードを引きます\n", p1, p2);

    /* どのカードを引くかを決める */
    c = getDrawCard(p1, p2);

    /* p2番目のプレイヤーの手札の一番後ろにc枚目のカードをp1番目のプレイヤーに追加する */
    players[p1].hand[players[p1].num_hand] = players[p2].hand[c];
    players[p1].num_hand++;

    /* p2番目のプレイヤーの手札からc枚目のカードを削除する */
    discard(p2, c);
    
    /* p1番目のプレイヤーの手札に追加されたカードの位置を返却 */
    return players[p1].num_hand - 1;
}

上記の draw 関数の返却値は、p1 番目のプレイヤーの手札に加わったカードの位置となります。

スポンサーリンク

引いたカードがペアになればカードを捨てる

上記の draw 関数でプレイヤーがカードを引いた後、プレイヤーは引いたカードが他の手札のカードとペアになっているかどうかを確認し、ペアになっている場合はカードを捨てることになります。

引いたカードが他の手札とペアになっているかどうかの判断は ペアになっているカードを探す で紹介した getPair 関数で、さらにペアになったカードを捨てる処理は ペアの2枚のカードを捨てる で紹介した discardPair 関数で既に実現済みのため、新たに関数等を新規作成する必要はありません。

カードを引く&カードを引かれるプレイヤーを決める

ただし、draw 関数に関しては「カードを引くプレイヤー」と「カードを引かれるプレイヤー」、getPair 関数と discardPair 関数では「カードを引くプレイヤー」を示す引数を指定する必要があります。

つまり、これらの関数を実行するためには、あらかじめ「カードを引くプレイヤー」と「カードを引かれるプレイヤー」を決定しておく必要があります。

続いては、これらのプレイヤーを決める方法について考えていきたいと思います。

基本的には、「カードを引くプレイヤー」と「カードを引かれるプレイヤー」はプレイヤーを順々に交代しながら順番に回していけば良いです。

より具体的には、p1 番目のプレイヤーを「カードを引くプレイヤー」とすれば、最初は p10 とし、プレイヤーがカードを引くたびに p11 ずつ増やしていけば良いことになります。プレイヤーの人数は NUM_PLAYER であるため、p1NUM_PLAYER - 1 の次は 0 に戻して「カードを引くプレイヤー」を回していくことになります。

また「カードを引かれるプレイヤー」を p1 + 1 番目のプレイヤーとすれば、p1 の変化に応じて「カードを引くプレイヤー」と「カードを引かれるプレイヤー」の両方が順々に変わりながらババ抜きがプレイされていくことになります(p1NUM_PLAYER - 1 の場合は 0 番目のプレイヤーを「カードを引かれるプレイヤー」とする)。

基本的には上記のように順々にプレイヤーの順番を回していけば良いのですが、カードを持っていないプレイヤーの順番はスキップさせる必要がある点には注意が必要になります。カードを持っていないプレイヤーは既に「勝ち」の状態なので、「カードを引くプレイヤー」として順番を回す必要もありませんし、「カードを引かれるプレイヤー」としても順番を回す必要がありません。

そのため、次に「カードを引くプレイヤー」と「カードを引かれるプレイヤー」を決める際には、候補となるプレイヤーがカードを持っているかどうかを判断し、持っていない場合は、そのプレイヤーの順番をスキップするような処理が必要になります。

プレイヤーの順番を決める際に手札のないプレイヤーをスキップする必要があることを示す図

こういったスキップの処理が必要となるため、今回は下記のような getNextPlayer 関数を用意し、この関数によって次のプレイヤーを決定するようにしたいと思います。

次のプレイヤーを決める
/* p番目のプレイヤー以降でカードを持っている最初のプレイヤーを求める */
int getNextPlayer(int p) {
    int i;
    for (i = 0; i < NUM_PLAYER; i++) {
        /* 最初に見つけたカードを持っているプレイヤーの番号を返却 */
        if (players[(p + i) % NUM_PLAYER].num_hand > 0) {
            return (p + i) % NUM_PLAYER;
        }
    }

    /* 手札が残っているプレイヤーが存在しない場合はエラー */
    return -1;
}

上記の getNextPlayer 関数は p 番目のプレイヤー以降で「カードを持っている最初のプレイヤー」を求める関数になっています。p 番目のプレイヤーがカードを持っていない場合、そのプレイヤーがスキップされるようになっているため、カードを持っていないプレイヤーをスキップしながら「カードを引くプレイヤー」と「カードを引かれるプレイヤー」を決めることができます。

「ゲーム終了」かどうかを判断する

ここまで紹介した関数により、各プレイヤーがカードを引いたりカードを捨てるような処理が実現できたことになります。そして、これらの処理をプレイヤーの順番を回しながら繰り返し行うことで、プレイヤーの手持ちがどんどん減っていき、最後にカードを持っているプレイヤーが一人のみになればゲーム終了となります。

最後に、この「ゲーム終了」かどうかを判断するための関数を作成していきたいと思います。

前述の通り、カードを持っているプレイヤーが一人のみになった際にババ抜きはゲーム終了となります。

各プレイヤーのnum_handとゲーム終了の関係を示す図

そのため、下記のようにカードを持っているプレイヤーの人数をカウントする関数により、ゲーム終了かどうかを判断するようにしていきたいと思います。

カードを持っているプレイヤー数を求める
/* まだ手札にカードが残っているプレイヤーの数を求める */
int getPlayerNum(void) {
    int count;
    int p;

    count = 0;

    for (p = 0; p < NUM_PLAYER; p++) {
        if (players[p].num_hand > 0) {
            count++;
        }
    }

    return count;
}

この getPlayerNum 関数の返却値が 1 である場合はカードを持っているプレイヤー数が 1 ということになるため、その場合にゲーム終了となるように繰り返し処理を終了するようにしてやれば良いことになります。

スポンサーリンク

ババ抜きのゲームの流れに従って関数を呼び出す

以上で、ババ抜きを実現するために必要となる関数が全て揃ったことになります。後はババ抜きのゲームの流れに従って用意した関数を呼び出してやれば良いだけです。

ババ抜きのゲームの流れを整理しておくと、まずババ抜きではカードのシャッフル(shuffle)が行われた後にカードが配られ(deal)、さらにその後に全プレイヤーがペアとなっているカードを捨てます(allDiscard)。

この後は、カードを引くプレイヤーとカードを引かれるプレイヤーの順番を回しながら(getNextPlayer)プレイヤーが他のプレイヤーのカードを引き(draw)、引いたカードがペアになっているかどうかを判断し(getPair)、さらにペアになっている場合はカードを捨てる(discardPair)という動作を繰り返し行なっていくことになります。

そして、この繰り返しはカードを持っているプレイヤーの人数(getPlayerNum)が 1 になるまで行われます。

ババ抜きのゲームの流れが上記のようになっているため、下記のように関数を呼び出すようにしてやれば、ババ抜きのゲームの流れをプログラムで実現することができることになります。

関数の呼び出し
int i;
int p1; /* カードを引くプレイヤーの番号 */
int p2; /* カードを引かれるプレイヤーの番号 */
int c1; /* 引いたカードの位置 */
int c2; /* c1の位置のカードとペアになるカードの位置 */

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

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

/* 既に揃っているカードを捨てる */
allDiscard();

p1 = 0;

/* カードを持っているプレイヤーが1以下になるまでループ*/
while (getPlayerNum() > 1) {

    /* 次にカードを引くプレイヤーを決める */
    p1 = getNextPlayer(p1);

    /* 次にカードを引かれるプレイヤーを決める */
    p2 = getNextPlayer(p1 + 1);

    /* p1番目のプレイヤーがp2番目のプレイヤーからカードを引く*/
    c1 = draw(p1, p2);

    /* 引いたカードと同じ数字のカードがあるかを調べる */
    c2 = getPair(p1, c1);

    if (c2 != -1) {
        /* 揃ったカードを捨てる */
        discardPair(p1, c1, c2);
    }

    /* 次にカードを引くプレイヤーの候補 */
    p1++;
}

「ババ抜き」のプログラム

最後に「ババ抜き」のプログラムのソースコード全体を紹介しておきます。

ソースコード

「ババ抜き」のプログラムのソースコードは下記のようになります。基本的には、ここまで紹介してきた関数と、その呼び出し部分をまとめたものになります。

ババ抜き
#include <stdio.h>
#include <stdlib.h> /* rand/srand */
#include <time.h> /* time */

#define NUM_SUIT 4 /* マークの種類(4以下を設定) */
#define NUM_NUMBER 13 /* 数字の種類(13以下を設定) */
#define NUM_PLAYER 4 /* プレイヤーの人数 */
#define NUM_CARD (NUM_SUIT * NUM_NUMBER + 1) /* カードの総数 */
#define HUMAN 0 /* 人がプレイするプレイヤーの番号 */
#define SHOW 0 /* 人以外のプレイヤーの手札を表示するかどうか */

/* カードを表す構造体 */
typedef struct _CARD {
    int suit; /* マークを表す整数 */
    int number; /* 数字を表す整数 */
} CARD;

/* プレイヤー表す構造体 */
typedef struct _PLAYER {
    CARD hand[NUM_CARD]; /* 手札のカードの配列 */
    int num_hand; /* 手札のカードの枚数 */
} PLAYER;

static PLAYER players[NUM_PLAYER]; /* プレイヤーの配列 */
static CARD cards[NUM_CARD]; /* カードの配列 */

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


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

/* ババ抜きゲームの初期化を行う */
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;
        }
    }

    /* 最後の一枚はジョーカーに設定 */
    cards[NUM_CARD - 1].suit = 4;
    cards[NUM_CARD - 1].number = 14;

    /* 各プレイヤーの持つカード数を0に設定 */
    for (i = 0; i < NUM_PLAYER; i++ ) {
        players[i].num_hand = 0;
    }
}

/* カード(cards配列)をシャッフルする */
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;
    int top = 0;
    int p = 0;

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

        /* カードの配り先となるプレイヤーを決定 */
        p = i % NUM_PLAYER;

        /* 配られたカードを手札の一番後ろにコピー */
        players[p].hand[players[p].num_hand] = cards[top];

        /* プレイヤーの手持ちカード枚数を1増やす */
        players[p].num_hand++;

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

/* 各プレイヤーの手札を表示する */
void show(void) {
    int i;
    int p;

    for (p = 0; p < NUM_PLAYER; p++) {
        printf("[プレイヤー%dの手札:%d枚]\n", p, players[p].num_hand);
        if (p == HUMAN || SHOW) {
            for (i = 0; i < players[p].num_hand; i++) {
                printf("%s:%s  ", suit_str[players[p].hand[i].suit], number_str[players[p].hand[i].number]);
            }
            printf("\n");
        }
    }
    printf("\n");
}

/* p番目のプレイヤーの手札のc枚目のカードと同じ番号のカードを取得する */
int getPair(int p, int c) {
    int i;

    for (i = 0; i < players[p].num_hand; i++) {
        if (i != c) {
            if (players[p].hand[i].number == players[p].hand[c].number) {
                /* i枚目のカードとc枚目のカードとは同じ番号なのでiを返却 */
                return i;
            }
        }
    }

    /* c枚目のカードと同じ番号のカードは手札に存在しないので-1を返却 */
    return -1;
}

/* p番目のプレイヤーの手札からc枚目のカードを捨てる */
void discard(int p, int c) {
    int i; 


    /* c+1枚目以降ののカードを1つ前側に移動 */
    for (i = c; i < players[p].num_hand - 1; i++) {
        players[p].hand[i] = players[p].hand[i + 1];
    }
    
    /* 手持ちのカード数を1減らす */
    players[p].num_hand--;

}

/* p番目のプレイヤーの手札からc1枚目とc2枚目のカードを削除*/
void discardPair(int p, int c1, int c2) {
    int i; 

    printf("プレイヤー%dが %s:%s と %s:%s を捨てました\n",
        p, suit_str[players[p].hand[c1].suit], number_str[players[p].hand[c1].number],
        suit_str[players[p].hand[c2].suit], number_str[players[p].hand[c2].number]
    );

    discard(p, c1);

    if (c1 < c2) {
        discard(p, c2 - 1);
    } else {
        discard(p, c2);
    }

}

/* p番目のプレイヤー以降でカードを持っている最初のプレイヤーを求める */
int getNextPlayer(int p) {
    int i;
    for (i = 0; i < NUM_PLAYER; i++) {
        /* 最初に見つけたカードを持っているプレイヤーの番号を返却 */
        if (players[(p + i) % NUM_PLAYER].num_hand > 0) {
            return (p + i) % NUM_PLAYER;
        }
    }

    /* 手札が残っているプレイヤーが存在しない場合はエラー */
    return -1;
}

/* p1番目のプレイヤーがp2番目のプレイヤーから引くカードを決める */
int getDrawCard(int p1, int p2) {
    int c;

    if (p1 == HUMAN) {
        /* プレイヤーが人の場合はscanfで引くカードを決める */
        do {
            printf("何枚目のカードを引きますか?:");
            scanf("%d", &c);
            /* 入力された整数が不正の場合は再度入力受付を行う */
        } while (c >= players[p2].num_hand || c < 0);

    } else {
        /* プレイヤーがコンピューターの場合はランダムに決める */
        c = rand() % players[p2].num_hand;
    }

    return c;
    
}

/* p1番目のプレイヤーがp2番目のプレイヤーからカードを引く */
int draw(int p1, int p2) {
    int c;
    int i;
    
    printf("プレイヤー%dがプレイヤー%dからカードを引きます\n", p1, p2);

    /* どのカードを引くかを決める */
    c = getDrawCard(p1, p2);

    /* p2番目のプレイヤーの手札の一番後ろにc枚目のカードをp1番目のプレイヤーに追加する */
    players[p1].hand[players[p1].num_hand] = players[p2].hand[c];
    players[p1].num_hand++;

    /* p2番目のプレイヤーの手札からc枚目のカードを削除する */
    discard(p2, c);
    
    /* p1番目のプレイヤーの手札に追加されたカードの位置を返却 */
    return players[p1].num_hand - 1;
}

/* 全プレイヤーの手札の中からペアになっているカードを全て捨てる */
void allDiscard(void) {
    int p, i, c1, c2;

    for (p = 0; p < NUM_PLAYER; p++) {
        c1 = 0;
        while (c1 < players[p].num_hand) {
            /* p番目のプレイヤーの手札からc1枚目のカードと同じ番号のカードを取得する */
            c2 = getPair(p, c1);
            if (c2 != -1) {
                /* 同じ番号のカードが存在した場合は、それらのカードを捨てる */
                discardPair(p, c1, c2);
            } else {
                /* 同じ番号のカードが存在しない場合は次のカードの位置をc1とする*/
                c1++;
            }
        }
    }
}

/* まだ手札にカードが残っているプレイヤーの数を求める */
int getPlayerNum(void) {
    int count;
    int p;

    count = 0;

    for (p = 0; p < NUM_PLAYER; p++) {
        if (players[p].num_hand > 0) {
            count++;
        }
    }

    return count;
}

int main(void) {

    int i;
    int p1; /* カードを引くプレイヤーの番号 */
    int p2; /* カードを引かれるプレイヤーの番号 */
    int c1; /* 引いたカードの位置 */
    int c2; /* c1の位置のカードとペアになるカードの位置 */

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

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

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

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

    /* 既に揃っているカードを捨てる */
    allDiscard();

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

    p1 = 0;

    /* カードを持っているプレイヤーが1以下になるまでループ*/
    while (getPlayerNum() > 1) {

        /* 次にカードを引くプレイヤーを決める */
        p1 = getNextPlayer(p1);

        /* 次にカードを引かれるプレイヤーを決める */
        p2 = getNextPlayer(p1 + 1);

        /* p1番目のプレイヤーがp2番目のプレイヤーからカードを引く*/
        c1 = draw(p1, p2);

        /* 引いたカードと同じ数字のカードがあるかを調べる */
        c2 = getPair(p1, c1);

        if (c2 != -1) {
            /* 揃ったカードを捨てる */
            discardPair(p1, c1, c2);
        }
        
        /* カードを表示 */
        show();

        /* 次にカードを引くプレイヤーの候補 */
        p1++;
    }

    return 0;
}

いくつか補足で解説しておくと、まず HUMAN という定数マクロで「人がプレイするプレイヤーの番号」を定義するようにしており、この HUMAN の定義値が 0NUM_PLAYER - 1 の場合、その番号のプレイヤーを人がプレイすることができます。それ以外の定義値の場合は、全てのプレイヤーをコンピューターがプレイすることになります。

また、show という関数を用意し、この関数の中で各プレイヤーの手札の枚数を表示するようにしています。また、人がプレイしているプレイヤーの手札の中身は表示されるようになっていますし、SHOW という定数マクロの定義値を 1 にした場合は、他のプレイヤーの手札の中身も表示されるようになっています。

スポンサーリンク

動作確認

最後に、上記のソースコードをコンパイルして生成したプログラムを実行した場合の動作を確認しておきたいと思います。

プログラムを実行すると、まず下記のように各プレイヤーの手札の枚数が表示され、さらに各プレイヤーが手札に存在するペアのカードを捨てていく様子が表示されることになります。人がプレイヤーの場合のみ、手札の枚数に加えて手札のカードも表示されるようになっています(表示されるカードは最初に配られたカードによって異なることになります)。

さらに、カードを捨て終わった後に、各プレイヤーの手札が再度表示されることになります。

[プレイヤー0の手札:14枚]
S:K  D:J  H:3  S:5  H:A  C:3  C:J  S:J  C:2  H:5  C:10  C:4  D:K  C:Q  
[プレイヤー1の手札:13枚]
[プレイヤー2の手札:13枚]
[プレイヤー3の手札:13枚]

プレイヤー0が S:K と D:K を捨てました
プレイヤー0が D:J と C:J を捨てました
〜略〜
プレイヤー3が S:4 と H:4 を捨てました
[プレイヤー0の手札:6枚]
H:A  S:J  C:2  C:10  C:4  C:Q  
[プレイヤー1の手札:7枚]
[プレイヤー2の手札:7枚]
[プレイヤー3の手札:5枚]

各プレイヤーがペアのカードを捨てた後は、続けて下記のようにカードを引くフェーズに移行し、プレイヤーが人の場合はどのカードを引くのかを尋ねられることになります。下記の場合はプレイヤー 00 番目のプレイヤー)がプレイヤー 11 番目のプレイヤー)からカードを引くことになりますので、06 の整数を入力して引くカードを決定することになります(上記の通り、プレイヤー 1 の手札は 7 枚)。

プレイヤー0がプレイヤー1からカードを引きます
何枚目のカードを引きますか?:

整数を入力してエンターキーを押すと指定したカードがプレイヤー間で移動し、次は引いたカードがペアになった場合のみカードを捨てる処理が行われます。さらに、他のプレイヤーに対しても同様にカードを引く処理が行われていきます(人以外のプレイヤーに関しては引くカードはランダムに決定されます)。

何枚目のカードを引きますか?:6
[プレイヤー0の手札:7枚]
H:A  S:J  C:2  C:10  C:4  C:Q  H:K  
[プレイヤー1の手札:6枚]
[プレイヤー2の手札:7枚]
[プレイヤー3の手札:5枚]

プレイヤー1がプレイヤー2からカードを引きます
[プレイヤー0の手札:7枚]
H:A  S:J  C:2  C:10  C:4  C:Q  H:K  
[プレイヤー1の手札:7枚]
[プレイヤー2の手札:6枚]
[プレイヤー3の手札:5枚]

プレイヤー2がプレイヤー3からカードを引きます
プレイヤー2が D:2 と S:2 を捨てました
[プレイヤー0の手札:7枚]
H:A  S:J  C:2  C:10  C:4  C:Q  H:K  
[プレイヤー1の手札:7枚]
[プレイヤー2の手札:5枚]
[プレイヤー3の手札:4枚]

プレイヤー3がプレイヤー0からカードを引きます
[プレイヤー0の手札:6枚]
H:A  S:J  C:2  C:10  C:4  C:Q  
[プレイヤー1の手札:7枚]
[プレイヤー2の手札:5枚]
[プレイヤー3の手札:5枚]

プレイヤー0がプレイヤー1からカードを引きます
何枚目のカードを引きますか?:

順番が一周まわった際には、再度カードの指定の入力受付が行われますので、同様に引きたいカードを整数で指定します。あとは、ゲームが終了するまで同様の動作が繰り返し行われることになります。

文字が出力されるだけではあるのですが、ババ抜きが実現できていることが確認できると思います!

まとめ

このページでは、C言語における「ババ抜き」の作り方について解説しました!

乱数の扱いなどはゲームを作る上では必須の知識となりますし、カードを引く、カードを捨てるような処理は色んなトランプゲームで応用できるテクニックだと思いますので、是非この辺りは考え方だけでも覚えておくと良いと思います。

今回開発した「ババ抜き」のような簡単そうなゲームであっても、実際に作ってみると案外苦労することも多く、さらに色んな気づきや学びも得られるのではないかと思います。

題材がゲームなので楽しみながら開発できますし、実際に自身が作ったもので遊ぶこともできるため、作れた時の充実感も大きいと思います。こういった面でゲームを作ってみるのはプログラミングの力をつけるのにオススメですので、色んなゲームの開発に挑戦してみていただければと思います!

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