このページでは、C言語での「ババ抜き」の作り方について解説していきます!
ババ抜きに関しては皆さんもプレイしたことがあるのではないでしょうか?
特にプログラミング初心者の方には「プレイしたことのある簡単なゲームをプログラミングして作ってみる」ことは非常にオススメです。プレイしたことのあるゲームであれば目標となるプログラムが明確ですし、実際にプログラミングすることで多くの気づきも得られ、更にいろんな処理の実現方法を考えていくことで今後プログラミングで使えるテクニック等も学ぶことができます。
もちろんゲームなので作っていて楽しいと思いますし、出来上がった際の充実感も大きいと思います。
今回は「ババ抜き」を題材にして作り方を解説していきますが、このページの解説内容を参考にして他のゲームの開発にも是非挑戦してみてください!
それでは、ババ抜きの作り方について解説していきたいと思います!最初は「ババ抜き」の流れのおさらいから説明していくことになりますので、すぐにプログラムを知りたいという方は 「ババ抜き」のプログラム までスキップしていただければと思います。
Contents
「ババ抜き」の流れ
では、まずはババ抜きのゲームの流れをおさらいしていきたいと思います。
自身がプレイしたことのあるゲームをプログラミングで開発する際には、まずはそのゲームで行われる動作の流れを考えてみるのが良いと思います。
そして、その流れの中で行われることを処理として実装し、さらにそれらを適切なタイミングで実行するようにすればゲームとして完成します。
ということで、まずはババ抜きのゲームとしての流れをおさらいしていきましょう!
まずババ抜きではカードがシャッフルされ、さらに各プレイヤーに対してカードが配られます。
カードが配られた後は、各プレイヤーが手札を確認してペアとなるカードを見つけ出し、その見つけたペアのカードを捨てます。
ここまでが前準備のようなもので、ここからはプレイヤーが隣のプレイヤーからカードを引き、引いたカードがペアになったのであればそのペアのカードを捨てます。ただし、カードを引く際には、事前にどのカードを引くのか?を決定しておく必要があります。
これらをプレイヤーの順番を回しながら繰り返していけば、次第にプレイヤーのカードが減っていくことになります。手札のカードがなくなったらそのプレイヤーは「勝ち」となります。そして、手札の残っているプレイヤーが一人になった際に勝負が決着し、最後まで手札にカードが残っていたプレイヤーが「負け」となります。
以上がババ抜きのゲームの流れであり、これらの流れを箇条書き形式で書き出せば下記のようになります。
- カードをシャッフルして配る
- 手札の中からペアになっているカードを捨てる
- 「カードを引く」&「カードを引かれる」プレイヤーを決める
- 引くカードを決める
- カードを引く
- 引いたカードがペアになればカードを捨てる
- 「ゲーム終了」かどうかを判断する
- ゲーム終了でなければ 3. に戻る
要は、これらの各処理を関数などで実現し、後は main
関数等から上記の流れが実現できるように各関数を呼び出してやればババ抜きが完成することになります。
「ババ抜き」に必要な処理の実現
ということで、先ほど挙げた各処理を関数として実現していきたいと思います。
が、処理を実現する前に、まずはババ抜きを実現する上での「データの扱い」について解説します。ババ抜きのプログラムではカードを扱いますし、さらにはプレイヤーを扱う必要もあります。これらをC言語で扱っていくための考え方について説明していきます。
そして、その後に、先ほど挙げた各処理の実現方法について解説していきたいと思います。
スポンサーリンク
トランプのカードを扱う
まず、ババ抜きはトランプゲームであり、トランプのカードを扱うゲームになります。
「何を当たり前のことをわざわざ…」と思われるかもしれませんが、ここは結構大事で、特にポイントになるのは「トランプのカードをどうやってプログラムの中で扱うか?」という点になります。
例えば、カードではなく単なる整数を扱うゲームなのであれば、扱う整数を int
型の変数などで管理すれば良いだけになります。
ですが、ババ抜きで扱われるのはトランプのカードです。C言語では数値や文字等を管理するための型は存在しますが、カードを扱うための型は存在しません。
カードを扱う型の定義
ですが、C言語では自身で新たな型を定義し、それで数値や文字以外のデータを扱うことが可能です。
具体的には、構造体や typedef
を利用することで「新たな型」を定義することが可能となります。構造体や typedef
については下記ページで解説していますので、詳しく知りたいかたは下記ページをご参照いただければと思います。
今回は、構造体と typedef
を利用してカードを扱うための型である CARD
を定義していきたいと思います。ババ抜きにおいて、トランプのカードで重要になるのはマークと数字になります。そのため、下記のようにマークを扱うメンバ suit
と数字を扱うメンバ number
を持つ型を CARD
型として定義していきます。
/* カードを表す構造体 */
typedef struct _CARD {
int suit; /* マークを表す整数 */
int number; /* 数字を表す整数 */
} CARD;
この CARD
型の変数1つ or CARD
型の配列の要素1つが1枚のトランプのカードを表現することになります。
53枚のカードの用意
さらに、ババ抜きで使用するカード全てが扱えるよう、カードの枚数(NUM_CARDS
)をサイズとする CARD
型の配列 cards
をグローバル変数として宣言しておきます。
念の為補足しておくと、ババ抜きで使用するトランプのカードは計53枚なので NUM_CARDS
は 53
となります(1
〜 13
までの数字のカードが4種類ずつ存在し、さらにジョーカーのカードが存在する)。
static CARD cards[NUM_CARD]; /* カードの配列 */
この cards
において、各要素が1枚のカードを表す CARD
型の要素となります。ただし、宣言しただけではマーク(suit
)と数字(number
)が各要素に設定されていないため、ゲームを開始する前に予めマークと数字を設定しておく必要があります。
このマークと数字の設定は、下記のような処理により実現することができます(NUM_SUIT
はマークの種類数 4
、NUM_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]
がジョーカーを表すカードとなり、ジョーカーであることが分かるように、他のマークや他の数字とは異なる値である 4
を suit
に、さらに 14
を number
に設定するようにしています。
上記のように処理を行えば、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 = 2
、cards[c1].number = 13
の場合は、上記によりスペードのキングを表す S:K
が表示されることになります。また、カードがジョーカーの場合はババであることがわかりやすいように B:B
が取得されるようにしています。
今回の例のように、プログラムの中では整数として扱っている場合でも、ユーザーに向けて表示を行う際には文字列に変換したいというケースは結構多いです。そして、このような場合は上記のような配列から文字列への変換結果を取得するテクニックが使えますので、この考え方は覚えておくと良いと思います。
複数人のプレイヤーを扱う
カードが扱えるようになったため、次はプレイヤーを扱えるようにしていきたいと思います。
プレイヤーを扱う型の定義
ババ抜きにおける各プレイヤーは手札にカードを持っており、各プレイヤーが他のプレイヤーからカードを引いたり、ペアになったカードを捨てたりするなどして、手札のカードを変化させながら進行していくゲームとなります。
実際にババ抜きをプレイをしている際は、最低限自身の手札のカードのみを管理していけば良いのですが、ババ抜きをプログラミングで開発する場合、全てのプレイヤーの手札のカードを管理する必要があります。
こういった各プレイヤーの手札のカードを管理していけるように、まず、下記のように PLAYER
型を定義したいと思います。
/* プレイヤー表す構造体 */
typedef struct _PLAYER {
CARD hand[NUM_CARD]; /* 手札のカードの配列 */
int num_hand; /* 手札のカードの枚数 */
} PLAYER;
PLAYER
型はプレイヤーの手札のカードを管理する hand
と手札のカードの枚数を管理する num_hand
の2つのメンバから構成される型となります。
hand
は CARD
型の配列であり、hand
のサイズである NUM_CARD
は前述の通りカードの枚数である 53
を示す定数マクロになります。プレイヤーの人数が一人の場合は最初に 53
枚のカードが配られることになるため、一応この最悪値を見越して hand
の要素数を設定しています。
このように、手札のカードとなる最悪値を配列 hand
のサイズとしていますが、実際にはプレイヤーの手札のカード枚数は num_hand
となります。したがって、例えば PLAYER
型の変数 player
の手札のカードは player.hand[0]
〜 player.hand[player.num_hand - 1]
のみとなります。
複数のプレイヤーの用意
さらに、下記のように PLAYER
型の配列 players
をグローバル変数として宣言することで、複数人のプレイヤーを管理できるようにしていきたいと思います。ここで NUM_PLAYER
はプレイヤーの人数を表す定数マクロとなります。
static PLAYER players[NUM_PLAYER]; /* プレイヤーの配列 */
この配列 players
の型は PLAYER
なので、players
の各要素はメンバとして hand
と num_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枚配る処理を行なっています。p
は i % NUM_PLAYER
で求めているため、繰り返しのたびに次のプレイヤーに対してカードが配られるようになっています。
もう少し具体的に説明すると、players[p].num_hand
は p
番目のプレイヤーにおける手札のカードの枚数であるため、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
関数を実行しておく必要がある点がポイントになると思います。
また、上記の shuffle
関数の中では Fisher–Yates shuffle というアルゴリズムに基づいてカードのシャッフルを行なっています。この Fisher–Yates shuffle は「重複無し」の乱数を生成する際に便利な考え方なので、特にゲームなどをプログラミングしたいと考えている方は覚えておくと良いと思います。下記ページで詳細を解説していますので、興味のある方は是非読んでみてください。
スポンサーリンク
手札の中からペアになっているカードを捨てる
カードが配られた後は各プレイヤーが手札の中でペアになっているカードを捨てていくことになります。ペアになっているカードとは「同じ番号の2枚のカード」になります。
このペアになっているカードを捨てる処理は下記ように2つの処理を段階的に実行して実現する必要があります。
- ペアになっているカードを探す
- ペアになっているカードを捨てる
つまり、まず手札の中からペアのカードを全て探し出し、見つかったペアのカードを捨てるような処理を行う必要があります。そして、これらの処理を全プレイヤーに対して実行する必要があります。
ペアになっているカードを探す
まずは特定のプレイヤーの手札の中から指定された1枚のカードと「ペアになっているカードを探す」処理を実現する方法について考えていきましょう。
このような処理は、その特定のプレイヤーの手札の中から “指定されたカードと同じ番号のカード” を探索することで実現することができます。
指定されたカード以外に同じ番号を持つカードが手札の中に存在する場合、指定されたカードと見つけたカードはペアとなるため、これら2つのカードに対して次に説明する「ペアになっているカードを捨てる」処理を行います。
指定されたカード以外に同じ番号を持つカードが手札の中に存在しない場合は、指定されたカードとペアになるカードが存在しないことになりますので「ペアになっているカードを捨てる」処理はスキップすることになります。
このような考え方に基づき、引数で与えられた p
番目のプレイヤーの手札の中から「c
番目の位置に存在するカードとペアになるカード」を探索する関数は下記のように実装して実現することができます(p
は 0
〜 プレイヤーの人数 - 1
、c
は 0
〜 p 番目のプレイヤーの手札のカード枚数 - 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
の中から探索を行なっています。
同じ番号のカードが見つかった場合は、そのカードの位置(players[p].hand
の添字)を返却し、見つからなかった場合は -1
を返却するようにしています。
今回は単純な方法で探索を行なっていますが、探索アルゴリズムを改善することで探索の効率化を図るようなことも可能です。この辺りは下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
【C言語】データの探索アルゴリズム(線形探索・二分探索)について解説1枚のカードを捨てる
続いてペアになっているカードを捨てる処理の実現方法について考えていきたいと思います。
まずはペアではなく、1枚のカードを捨てる処理について考えていきましょう!
ここでは、p
番目のプレイヤーの手札から1枚のカードを捨てる処理について考えていきたいと思います。すなわち、players[p].hand
の配列から捨てたいカードを削除する処理を実現していきます。
また、今回は players[p].hand
の c
枚目のカードを捨てることを前提に解説をしていきます。c
は 0 ≦ c < players[p].num_hand
を満たす整数であり、players[p].num_hand
は p
番目のプレイヤーの手札のカードの枚数となります。
カードを捨てる処理の実現方法はいくつか存在するのですが、今回は c
枚目のカードよりも後ろ側に存在するカードを1枚分ずつ前側に移動させることでカードを捨てる処理を実現したいと思います。これにより、c
枚目のカードは c + 1
枚目のカードで上書きされるため、c
枚目のカードはプレイヤーの手札から存在しなくなります(下の図は players[p].num_hand
が 8
であり、さらに c=2
枚目のカードを捨てる際の処理を説明する図になります)。
さらに、p
番目のプレイヤーの手札のカードを捨てるのですから、p
番目のプレイヤーの手札の枚数は 1
減ることになります。そのため、上記の上書き処理を行なった後に players[p].num_hand--
を実行します。
これにより、手札から c
枚目のカードが存在しなくなり、さらに手札のカードの枚数も 1
減ることになるため、カードを捨てる処理が実現できていることになります。
このようなカードを捨てる処理は、下記の 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)
を実行すれば、c2
が c1
よりも大きい場合、c2
枚目のカードは c2 - 1
枚目のカードの位置に移動することになります。
したがって、c1
の位置のカードと c2
の位置のカードを捨てる際には、discard(p, c1)
の実行による c2
の位置のカードの移動の有無を判断し、移動した場合は discard(p, c2 - 1)
を、移動していない場合は discard(p, c2)
を実行するように処理を分岐する必要があります。
この判断は c1
と c2
の大小関係により実施することができ、ペアとなった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
ループにより、ループの内側の処理が全プレイヤーに対して繰り返し行われることになります。
さらに、内側の while
ループはプレイヤーの手札の各カードに対するループになっています。このループでは、手札の各カードに c1
対し、ペアになっているカードが存在するかどうかの判断を getPair
関数で実施し、ペアが存在する場合はペアとなっているカードの2枚を discardPair
で捨てる処理を行なっています。
ペアになっているカードを探す で解説している通り、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++
を実行するかどうかを切り替えるようにする必要があり、これを行うために、内側のループがちょっと複雑になっています。
以上の処理により、カードが配られた後に実施する「ペアになっているカードを捨てる処理」が実現できたことになります。
引くカードを決める
カードが配られた後に各プレイヤーがペアになっているカードを捨てた後は、プレイヤーの順番を回しながら他のプレイヤーの手札からカードを引いていくことになります。
カードを引く際には、事前に引くカードを決めておく必要があります。ということで、次は「引くカードを決める」処理の実現方法について考えていきましょう!
まず、p1
と p2
とが指定された際に、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;
}
上記では、p1
が HUMAN
という定数マクロと一致する場合に、その p1
番目のプレイヤーが人であると判断し、その場合に scanf
関数を実行して整数の入力受付を行うようにしています。
ただし、入力された値が 0
〜 players[p2].num_hand - 1
の整数でない場合は、入力された値が p2
番目のプレイヤーの手札のカードの位置として不適切であるため、再度 scanf
関数での入力受付を行うようにしています(p2
番目のプレイヤーは players[p2].num_hand
枚しか手札にカードを持っていない)。この手札のカードの位置は 0
から始まるという点に注意してください。
プレイヤーがコンピューターの場合
p1
番目のプレイヤーがコンピューターの場合は自動的にランダムに引くカードを決定してやれば良いです。ランダムに値を決定するのに便利なのが下記ページで解説している rand
関数であり、さらに rand() % players[p2].num_hand
のように剰余算を実行することで 0
〜 players[p2].num_hand - 1
の整数を取得することができます。
“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
番目のプレイヤーの手札にカードを加える処理について考えていきましょう。
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)
を実行すれば良いだけになります(c
は p1
番目のプレイヤーから引かれる p2
番目のプレイヤーの手札上の位置)。
このように、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
番目のプレイヤーを「カードを引くプレイヤー」とすれば、最初は p1
を 0
とし、プレイヤーがカードを引くたびに p1
を 1
ずつ増やしていけば良いことになります。プレイヤーの人数は NUM_PLAYER
であるため、p1
が NUM_PLAYER - 1
の次は 0
に戻して「カードを引くプレイヤー」を回していくことになります。
また「カードを引かれるプレイヤー」を p1 + 1
番目のプレイヤーとすれば、p1
の変化に応じて「カードを引くプレイヤー」と「カードを引かれるプレイヤー」の両方が順々に変わりながらババ抜きがプレイされていくことになります(p1
が NUM_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
番目のプレイヤーがカードを持っていない場合、そのプレイヤーがスキップされるようになっているため、カードを持っていないプレイヤーをスキップしながら「カードを引くプレイヤー」と「カードを引かれるプレイヤー」を決めることができます。
「ゲーム終了」かどうかを判断する
ここまで紹介した関数により、各プレイヤーがカードを引いたりカードを捨てるような処理が実現できたことになります。そして、これらの処理をプレイヤーの順番を回しながら繰り返し行うことで、プレイヤーの手持ちがどんどん減っていき、最後にカードを持っているプレイヤーが一人のみになればゲーム終了となります。
最後に、この「ゲーム終了」かどうかを判断するための関数を作成していきたいと思います。
前述の通り、カードを持っているプレイヤーが一人のみになった際にババ抜きはゲーム終了となります。
そのため、下記のようにカードを持っているプレイヤーの人数をカウントする関数により、ゲーム終了かどうかを判断するようにしていきたいと思います。
/* まだ手札にカードが残っているプレイヤーの数を求める */
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
の定義値が 0
〜 NUM_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枚]
各プレイヤーがペアのカードを捨てた後は、続けて下記のようにカードを引くフェーズに移行し、プレイヤーが人の場合はどのカードを引くのかを尋ねられることになります。下記の場合はプレイヤー 0
(0
番目のプレイヤー)がプレイヤー 1
(1
番目のプレイヤー)からカードを引くことになりますので、0
〜 6
の整数を入力して引くカードを決定することになります(上記の通り、プレイヤー 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言語における「ババ抜き」の作り方について解説しました!
乱数の扱いなどはゲームを作る上では必須の知識となりますし、カードを引く、カードを捨てるような処理は色んなトランプゲームで応用できるテクニックだと思いますので、是非この辺りは考え方だけでも覚えておくと良いと思います。
今回開発した「ババ抜き」のような簡単そうなゲームであっても、実際に作ってみると案外苦労することも多く、さらに色んな気づきや学びも得られるのではないかと思います。
題材がゲームなので楽しみながら開発できますし、実際に自身が作ったもので遊ぶこともできるため、作れた時の充実感も大きいと思います。こういった面でゲームを作ってみるのはプログラミングの力をつけるのにオススメですので、色んなゲームの開発に挑戦してみていただければと思います!