【C言語】じゃんけんゲームの作り方(ソースコードの可読性向上についても解説)

じゃんけんゲームの作り方の解説ページアイキャッチ

このページでは、C言語での「じゃんけんゲーム」の作り方について解説していきます。

今回開発するじゃんけんは1人プレイ用のゲームで、対戦相手はコンピューターとし、下記のようにプレイヤーが出す手(グー・チョキ・パー)を入力するとじゃんけんの結果が表示されるという簡単なものになります。

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):1

あなたが出した手:チョキ
相手が出した手 :チョキ

結果は・・・引き分けです!!!

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):2

あなたが出した手:パー
相手が出した手 :チョキ

結果は・・・あなたの負けです...

レベル的には入門書等を読んで一通りC言語について学んだ方向けの解説ページになります。

じゃんけんゲームを実際に作ってみることで、ループや条件分岐等の基礎構文についても復習できますし、ゲーム開発で必須となる乱数の扱いについても学ぶことができます。

また、おそらくこのページの読者はC言語入門者の方が多いと思いますので、プログラミングしていく上で重要になる「ソースコードの可読性」についても解説していきたいと思います。enum や関数等の利用によって、ソースコードの可読性が向上していく様子を実感していただければと思います!

じゃんけんゲームの作り方

では、じゃんけんゲームの作り方について解説していきたいと思います!

じゃんけんゲームの流れ

まず現実世界で2人でプレイするじゃんけんの流れについて思い出してみましょう!

おそらく下記のような流れになると思います(このページでは「あいこ」のことを引き分けと記載されていただきます)。

  1. 各プレイヤーが出す手(グー・チョキ・パー)を決める
  2. 各プレイヤーが決めた手を出す
  3. じゃんけんの結果(勝ち・負け・引き分け)を判断する
  4. じゃんけんの結果を言う
  5. 結果が「引き分け」の場合は最初に戻る

前述の通り、今回開発するじゃんけんのプレイヤーは2人ではなく、プレイヤー対コンピューターのゲームとなります。

開発するじゃんけんにおけるプレイヤーの説明を行う図

プレイヤー対コンピューターのじゃんけんゲームをプログラムで実現する場合、プログラムの動作の流れは下記のようになります。

  1. コンピューターが出す手を決定する
  2. プレイヤーが出す手の入力を受け付ける
  3. プレイヤーとコンピューターが出した手を表示する
  4. じゃんけんの結果を判断する
  5. じゃんけんの結果を表示する
  6. 結果が「引き分け」の場合は最初に戻る

基本的には、先ほど考えた現実世界でプレイヤー2人がプレイするじゃんけんのときと流れは同じようなものになります。

ただ、現実世界で行う「手を出す」「言う」といった行為の代わりに、プログラムでは表示を行う必要があります。

また、プレイヤー2人がじゃんけんをする場合は各自が出す手はプレイヤー自身が決めれば良いのですが、対戦相手がコンピューターの場合、プログラム内でコンピューターの出す手を決める必要があります。

さらに、じゃんけんの結果をプレイヤーではなくプログラム内で判断する必要もあります。実際は、プレイヤーとコンピューターが出した手からプレイヤー自身が結果を判断することもできるのですが、プログラム内で判断して表示してあげた方が親切です。

こういった風に、ゲームをプログラムで開発する際には、現実世界ではプレイヤーが行う動作をプログラム内で実行されるようにプログラミングし、さらに、現実世界ではプレイヤーが判断することをプログラム内で判断するようにプログラミングする必要があります。

この辺りがゲームを開発する上での難しさ&楽しさになるかなぁと思います。

この辺りも踏まえながら、ここから上記で挙げた 1. 〜 6. の動作をプログラムで実現し、じゃんけんゲームを実現していく手順を解説していきたいと思います。

スポンサーリンク

プログラム内部でのデータの扱い

先ほど挙げた 1. 〜 6. の動作をプログラムで実現していくために、まずはじゃんけんゲームでの「プログラム内部におけるデータの扱い」について解説しておきたいと思います。

要は、グー・チョキ・パーなどの手をどうやってプログラム内部で表現して扱うのか、と言う点の解説になります。

現実世界でじゃんけんを行う場合、手の形によってグー or チョキ or パーが判断されますが、プログラムで手の形を判断するのは難易度が高いため、下記のように単なる整数でグー・チョキ・パーを扱うようにしていきたいと思います。

  • グー:0
  • チョキ:1
  • パー:2

つまり、プレイヤーから出す手を入力してもらう際には、上記の 02 の整数を入力してもらい、入力してもらった整数に応じた手としてプログラム内部で扱っていきます。

例えばプレイヤーが 1 を入力した場合はプレイヤーがチョキを出したと判断し、さらに、コンピューターの出した手が 0 ならプレイヤーの負け(チョキ対グー)、コンピューターの出した手が 2 ならプレイヤーの勝ち(チョキ対パー)といった具合で結果の判断を行なっていくことになります。

同様に、じゃんけんの結果も下記のように整数で扱っていきたいと思います。

  • 引き分け:0
  • プレイヤーの勝ち:1
  • プレイヤーの負け:2

こんな感じで、ゲームに限らずプログラムを開発する際には、現実世界の現象をどのようなデータとしてプログラム内で扱うのかをしっかり決めてからプログラミングしていく必要があります。

コンピューターが出す手を決定する

プログラム内でのデータの扱いが決まったところで、次は実際にじゃんけんゲームの流れをプログラムで実現していきたいと思います。

まずはコンピューターが出す手を決定する動作を実現していきます。

あなたがゲームのプレイヤーなのであれば、コンピューターが出す手は自動的に決定されるので何をしなくても良いです。ですが、ゲームを開発する場合は別で、コンピューターの出す手がプログラム動作中に決定されるよう、あなたがコンピューターの出す手を決めるための処理をプログラミングしてやる必要があります。

コンピューターの出す手をプログラム内で決定する様子

ということで、コンピューターの出す手を決めるための処理をプログラミングしていきたいと思います。

まず、コンピューターが出す手が毎回同じものであるとゲーム性がなくなってしまいますので、ランダムに決定するようにしたいと思います。このようにランダムな決定を行う際に便利なのが乱数です。

そして、C言語で乱数を利用する際には、下記ページで解説している rand 関数を利用します。

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

ただし、rand 関数は 0RAND_MAX という定義値までの整数をランダムに返却する関数です。RAND_MAXstdlib.h で定義されている値で、私の環境では 0x7fffffff で定義されています。

それに対し、今回ランダムに取得したい値はグー・チョキ・パーを表す 02 の整数ですので、rand 関数の返却値をそのまま利用するのではなく、rand 関数の返却値に対して 3 での剰余算を計算することで 02 の乱数に変換してやるような処理が必要になります。

また、これも上記ページで解説しているのですが、プログラム起動ごとに異なる乱数を取得できるようにするためには、rand 関数実行前に srand 関数を実行しておく必要があるので注意してください。

ここまでをまとめると、下記によってコンピューターの出す手を決めることができます。

コンピューターの出す手を決める

int b;

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

// コンピューターが出す手を決定する
b = rand() % 3;
}

上記によって変数 b02 の整数が格納されますので、 プログラム内部でのデータの扱い で解説したように、この変数 b に格納された整数を「コンピューターの出した手」として扱っていくことになります。

以降でも、上記のように実現する処理ごとに実装例を紹介していく形で解説していきたいと思います。後で全ソースコードをまとめて紹介します。

また、あまりにも変数名がてきとうですが、これはわざとです。

後述の ソースコードの可読性を向上させる で変数名を改善し、それによってどのようにソースコードの可読性が向上するかを実感していただくために、ひとまず上記のようなてきとうな変数名としています。この点はご了承いただければと思います。

プレイヤーが出す手の入力を受け付ける

続いて、プレイヤーが出す手を決める処理を実現していきます。

コンピューターが出す手を決める際には、それがプログラム内で決定されるようにプログラミングする必要がありましたが、プレイヤーが出す手はプレイヤーに決めてもらえば良いです。

ですので、基本的にプログラムとしては、そのプレイヤーが決めた手をプレイヤーがプログラムに伝えることができるよう、入力の受付をしてやれば良いだけになります。

プレイヤーの出す手をプログラムから入力受付する様子

この入力の受付はお馴染みの scanf 関数により実現できます。

ただし、ここで想定しておかなければならないのは「プレイヤーが開発者の求める値を入力してくれるとは限らない」という点になります。

プログラム内部でのデータの扱い で解説したように、じゃんけんの手(グー・チョキ・パー)は 02 の整数として扱いますので、プレイヤーから入力してもらいたい値も 02 の整数となります。チョキを出すのであれば 1 を入力してもらうことになります。

ですが、プレイヤーが 02 の整数以外のものを入力する可能性は当然あります。例えばプログラムの仕様を理解していなくて 3 を入力したり、間違って 7 を入力するようなこともあり得ます。

プレイヤーが間違って入力を行う様子

こういった、入力して欲しい値以外のものをプレイヤーが入力する可能性も考慮し、例えば 02 の整数以外の整数が入力された場合には再度入力受付を行うような処理が必要になります。

要は、入力して欲しい値が入力されない限りループして入力受付を行うことになります。

この辺りも考慮すると、プレイヤーが出す手の入力を受け付ける処理は下記のように実装することができると思います。

プレイヤーが出す手の入力を受け付ける

int a;

// プレイヤーが出す手の入力を受け付ける
while (1) {
    printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
    scanf("%d", &a);

    if (a >= 3 || a < 0) {
        printf("出した手がおかしいです...\n\n");
    } else {
        break;
    }
}

上記により、while ループを抜けた際には a にプレイヤーが出した手が 02 の整数として格納されていることになります。

上記では、scanf でプレイヤーから整数の入力受付を行い、入力された整数が 02 でなければ再度入力受付を行うようにしています。

再度入力受付を行うために scanf や入力された整数のチェックは while ループの中で実行し、入力された整数が 02 である場合に breakwhile ループを抜け出すようにしています。

while ループの継続条件は 1 としているため、break が実行されない限り無限ループすることになり、02 の整数が入力されるまで何度でも入力受付を行うことができるようになっています。

break に関しては下記ページで解説していますので、ご存知ない方は下記ページを参考にしていただければと思います。

breakとcontinueの解説ページアイキャッチ【C言語】breakとcontinueについて解説(ループを抜け出す・ループをスキップする)

ただ、一点補足しておくと、上記の while ループでは scanf に整数以外が入力された際にプログラムが異常な動作をすることになるので注意してください(具体的には無限ループとなります)。これを防ぐためには scanf で整数ではなく文字列の入力を受け付け、さらにその文字列を整数に変換してからチェックを行うような処理が必要になります。

今回はそこまでの対応例は示しませんが、具体的な対応例は下記ページで紹介しているのでこちらを参考にしていただければと思います。

scanf利用時に発生する無限ループの対策方法解説ページアイキャッチ【C言語】scanf利用時に発生する無限ループの対策方法

スポンサーリンク

プレイヤーとコンピューターが出した手を表示する

プレイヤーとコンピューターの手が出揃ったところで、続いて各々が出した手の表示を行いたいと思います。

出された手を表示する様子

プレイヤーとコンピューターが出した手は、プログラム内部でのデータの扱い で解説したようにプログラム内では 02 の整数で扱われています。

これをそのまま表示してやっても良いのですが、下記のように文字列に置き換えて表示してあげた方が視覚的に分かりやすいため、文字列として表示を行うようにしたいと思います。

  • 0:グー
  • 1:チョキ
  • 2:パー

そのため、下記のように if 文で条件分岐を行い、それぞれの整数に応じた文字列を表示を行うようにしたいと思います。

出した手を表示する

// プレイヤーとコンピューターが出した手を表示する
printf("あなたが出した手:");
if (a == 0) {
    printf("グー\n");
} else if (a == 1) {
    printf("チョキ\n");
} else if (a == 2) {
    printf("パー\n");
}

printf("相手が出した手 :");
if (b == 0) {
    printf("グー\n");
} else if (b == 1) {
    printf("チョキ\n");
} else if (b == 2) {
    printf("パー\n");
}
printf("\n");

じゃんけんの結果を判断する

続いて、プレイヤーとコンピューターの出した手からじゃんけんの結果の判断を行なっていきたいと思います。

じゃんけんの結果を判断する様子

ここはじゃんけんゲームにおける1つのポイントになると思います。

今回は、プレイヤーの視点から見た結果(引き分け・勝ち・負け)の判断を行なっていくようにしたいと思います。

そして、判断結果を「じゃんけんの結果を格納する変数」に格納していきます。とりあえずですが、この変数は r としたいと思います。

さらに、プログラム内部でのデータの扱い で解説したように、プログラム内部ではじゃんけんの結果を下記のように扱います。

  • 引き分け:0
  • プレイヤーの勝ち:1
  • プレイヤーの負け:2

つまり、判断結果に応じて変数 r に上記の整数を格納することになります。

それでは、プレイヤーとコンピューターが出した手から、どのような処理を行なって結果を判断すれば良いのかについて解説していきます。

引き分けの判断

まず1番簡単なのが「引き分け」のパターンです。

これは単に、プレイヤーとコンピューターが出した手が同じであるかどうかで判断できます。

プレイヤーが出した手は a に、コンピューターが出した手は b にそれぞれ格納されているわけですから、これらが一致するかどうかを a == b により判断すれば良いということですね!

また、引き分けを表すデータは整数 0 ですので、下記のように a == b が成立した場合に r = 0 としてやれば良いことになります。

引き分けの判断

int r;

if (a == b) {
    r = 0;
}

勝ちの判断

次に、プレイヤーが「勝ち」である場合の判断について考えていきましょう。

引き分けの判断に比べるとちょっと難易度は上がりますが、要はプレイヤーが勝ちとなるパターンを列挙し、そのパターンに一致するかどうかを if 文で判断してやれば良いです。

具体的には、まずプレイヤーが勝ちとなるのは、プレイヤー対コンピューターの場合は下記の3つのパターンのみとなります。

  • プレイヤーが出した手が「グー」かつコンピューターが出した手が「チョキ」
  • プレイヤーが出した手が「チョキ」かつコンピューターが出した手が「パー」
  • プレイヤーが出した手が「パー」かつコンピューターが出した手が「グー」

つまり、プレイヤーが出した手とコンピューターが出した手が上記のいずれかの1つのパターンに当てはまれば「プレイヤーの勝ち」と判断することができることになります。

そのため、プレイヤーが出した手が格納される a とコンピューターが出した手が格納される b が上記の1つでも満たすかどうかを判断し、満たす場合に「勝ち」と判断してやれば良いことになります。そして、その場合に r = 1 を実行するようにします。

この辺りを踏まえれば、「プレイヤーの勝ち」の判断は、下記の else if 以降の処理によって実現することができることになります。

勝ちの判断

int r;

if (a == b) {
    r = 0;
} else if (
    a == 0 && b == 1 ||
    a == 1 && b == 2 ||
    a == 2 && b == 0
) {
    r = 1;
}

ちょっと else if の中の条件式がややこしいですね…。

一応補足しておくと、この条件式の中で使用している演算子の優先度は下記のようになります。

==&&||

このため、例えば1行目に注目すれば、最初に下記の判断が行われることになります。

  • a == 0
  • b == 1

次に下記の判断が行われます。a == 0b == 1 の両方が成立している場合のみ、下記についても成立していることになります。つまり 1行目の結果 が成立となります。

  • a == 0 の結果 && b == 1 の結果

そして、最後に他の行も含めて下記の判断が行われることになります。そして、いずれかの行が YES の場合(成立している場合)に if 文の中の条件式が成立したことになり r = 1 が実行されます。

  • 1行目の結果 || 2行目の結果 || 3行目の結果

ちょっと比較が入り組んでいて難しいですが、優先度をしっかり意識して考えればどのように判断が行われているかは理解していただけるのではないかと思います。

負けの判断

さて、ここまでプレイヤーが引き分けの場合と勝ちの場合の判断の仕方について解説してきました。

次は「負け」の判断を行なっていきます。

といっても、じゃんけんゲームの結果は引き分け・勝ち・負けの3パターンのみであり、前者2つについては既に判断できるようになっているため、前者2つに該当しなかった場合が「負け」と判断することができます。

したがって、引き分けや勝ちの判断を行なっている if 文に対し、else 節を加えてやれば「負け」の場合の処理を実現することができることになります。

そして、負の場合に r = 2 を実行するようにします。

具体的には、先ほど紹介した引き分け・勝ちを判断する if 文に対して下記のように else 節を加えれば、「負け」の判断及び「負け」の時の処理を実現することができるようになります。

これによって全てのじゃんけんの結果の判断ができるようになったことになります。

じゃんけんの結果の判断

int r;

if (a == b) {
    r = 0;
} else if (
    a == 0 && b == 1 ||
    a == 1 && b == 2 ||
    a == 2 && b == 0
) {
    r = 1;
} else {
    r = 2;
}

じゃんけんの結果を表示する

じゃんけんの結果が判断できたので、次はじゃんけんの結果を表示していきましょう!

じゃんけんの結果を表示する様子

じゃんけんの結果を判断する で紹介した処理によって変数 r にじゃんけんの結果を格納されることになりますので、r0 の場合に「引き分け」であること、r1 の場合に「プレイヤーの勝ち」であること、r2 の場合に「プレイヤーの負け」であることをそれぞれ表示してやれば良いことになります。

そしてこれは、下記のような if 文によって実現することができます。

じゃんけんの結果を表示する

// じゃんけんの結果を表示する
printf("結果は・・・");
if (r == 0) {
    printf("引き分けです!!!\n");
} else if (r == 1) {
    printf("あなたの勝ちです!!!\n");
} else {
    printf("あなたの負けです...\n");
}
printf("\n");

スポンサーリンク

結果が「引き分け」の場合は最初に戻る

ここまでじゃんけんゲームにおける一連の流れを実現できたことになるのですが、じゃんけんでは引き分けが存在し、引き分けの場合はここまで紹介してきた一連の流れを繰り返し行うことになります。

これを実現するため、ここまで紹介してきた処理をループの中で実行するようにしていきたいと思います。

ループの継続条件は「じゃんけんの結果が引き分けである」となり、じゃんけんの結果が引き分けであるかどうかは r == 0 で判断できるため、下記のようなループの中でここまで紹介してきた処理を実行するようにしてやれば良いことになります。

引き分けの場合のループ

int a;
int b;
int r;

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

do {
    // コンピューターが出す手を決定する

    // プレイヤーが出す手の入力を受け付ける

    // プレイヤーとコンピューターが出した手を表示する

    // じゃんけんの結果を判断する

    // じゃんけんの結果を表示する

} while (r == 0);

上記の do while ループの中で、最初に紹介した次の処理を実行することになります。

  1. コンピューターが出す手を決定する
  2. プレイヤーが出す手の入力を受け付ける
  3. プレイヤーとコンピューターが出した手を表示する
  4. じゃんけんの結果を判断する
  5. じゃんけんの結果を表示する

また、これに関してはそこまでこだわる必要はないと思いますが、じゃんけんの場合は必ずここまで紹介してきた一連の処理を一度は実行することになるため、while よりも do while の方が綺麗にループが書けると思います(前述のプレイヤーが出す手の入力に関しても do while で実現しても良かったかも…)。

while と do while の使い分けについては下記ページで解説していますので、詳しくはこちらを参考にしていただければと思います。

whileとdo whileの違いの解説ページアイキャッチ【C言語】while と do while の違い

じゃんけんゲームのサンプルプログラム

ここまで解説してきた内容により、じゃんけんゲームを実現することができるようになりました。

ここで、ここまで解説してきた処理を組み合わせた「じゃんけんゲームのサンプルプログラム」のソースコード全体を紹介しておきます。

じゃんけんゲーム

#include <stdio.h> // printfとscanf
#include <stdlib.h> // srandとrand
#include <time.h> // time

int main(void) {

    int a;
    int b;
    int r;

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

    do {
        // コンピューターが出す手を決定する
        b = rand() % 3;

        // プレイヤーが出す手の入力を受け付ける
        while (1) {
            printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
            scanf("%d", &a);

            if (a >= 3 || a < 0) {
                printf("出した手がおかしいです...\n\n");
            } else {
                break;
            }
        }
        printf("\n");

        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        if (a == 0) {
            printf("グー\n");
        } else if (a == 1) {
            printf("チョキ\n");
        } else if (a == 2) {
            printf("パー\n");
        }

        printf("相手が出した手 :");
        if (b == 0) {
            printf("グー\n");
        } else if (b == 1) {
            printf("チョキ\n");
        } else if (b == 2) {
            printf("パー\n");
        }
        printf("\n");

        // じゃんけんの結果を判断する
        if (a == b) {
            r = 0;
        } else if (
            a == 0 && b == 1 ||
            a == 1 && b == 2 ||
            a == 2 && b == 0
        ) {
            r = 1;
        } else {
            r = 2;
        }

        // じゃんけんの結果を表示する
        printf("結果は・・・");
        if (r == 0) {
            printf("引き分けです!!!\n");
        } else if (r == 1) {
            printf("あなたの勝ちです!!!\n");
        } else {
            printf("あなたの負けです...\n");
        }
        printf("\n");

    } while (r == 0);

}

上記ソースコードをコンパイルして実行すれば下記のようなメッセージが表示されると思います。

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

このメッセージが表示された後に、下記の出したい手に応じた整数を入力してエンターキーを押してみてください。

  • 0:グー
  • 1:チョキ
  • 2:パー

エンターキーを押せば、あなたが出した手とコンピューターの出した手に応じた結果が表示されるはずです。例えば勝ち or 負けの場合は下記のようなメッセージが表示されてプログラムが終了します。

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):1

あなたが出した手:チョキ
相手が出した手 :グー

結果は・・・あなたの負けです...

それに対し、引き分けの場合は下記のようなメッセージが表示され、再度じゃんけんを行うことになります。

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):1

あなたが出した手:チョキ
相手が出した手 :チョキ

結果は・・・引き分けです!!!

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

また、じゃんけんの手を出す際に 02 以外の整数を入力すれば、警告メッセージが表示されて再度整数の入力受付が行われることになります。

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):3
出した手がおかしいです...

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

以上のような動作結果により、じゃんけんゲームが開発できていることを確認していただけると思います。

ただ、上記のソースコードは非常に読みにくいですね…。おそらく、ここまでの解説が無かったり、コメントがなかったりすると、さらにソースコードは読みにくくなると思います。

今回は単純なソースコードなのでまだ良いですが、規模の大きい複雑なソースコードになると処理を理解するのが困難になってしまうような場合もあり得ます。

ここからはおまけの解説になりますが、ソースコードを変更例を示し、ソースコードの可読性を向上させていきたいと思います。これにより、変数名や enum などの重要性も理解していただけるのではないかと思います!

ソースコードの可読性を向上させる

では、じゃんけんゲームのサンプルプログラム で紹介したソースコードを変更し、ソースコードの可読性を向上させる例を示していきたいと思います。

スポンサーリンク

変数名を工夫する

まずは、変数名を分かりやすい名前に変更していきたいと思います。今まで変数には abr といったてきとうな名前をつけていましたが、これらを下記のように変更したいと思います。

  • aplayer_hand
  • bcom_hand
  • rresult

変数名を変更した場合、じゃんけんゲームのサンプルプログラム で紹介したソースコードは下記のようになります。

変数名を工夫する

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {

    int player_hand;
    int com_hand;
    int result;

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

    do {
        // コンピューターが出す手を決定する
        com_hand = rand() % 3;

        // プレイヤーが出す手の入力を受け付ける
        while (1) {
            printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
            scanf("%d", &player_hand);

            if (player_hand >= 3 || player_hand < 0) {
                printf("出した手がおかしいです...\n\n");
            } else {
                break;
            }
        }
        printf("\n");

        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        if (player_hand == 0) {
            printf("グー\n");
        } else if (player_hand == 1) {
            printf("チョキ\n");
        } else if (player_hand == 2) {
            printf("パー\n");
        }

        printf("相手が出した手 :");
        if (com_hand == 0) {
            printf("グー\n");
        } else if (com_hand == 1) {
            printf("チョキ\n");
        } else if (com_hand == 2) {
            printf("パー\n");
        }
        printf("\n");

        // じゃんけんの結果を判断する
        if (player_hand == com_hand) {
            result = 0;
        } else if (
            (player_hand == 0 && com_hand == 1) ||
            (player_hand == 1 && com_hand == 2) ||
            (player_hand == 2 && com_hand == 0)
        ) {
            result = 1;
        } else {
            result = 2;
        }

        // じゃんけんの結果を表示する
        printf("結果は・・・");
        if (result == 0) {
            printf("引き分けです!!!\n");
        } else if (result == 1) {
            printf("あなたの勝ちです!!!\n");
        } else {
            printf("あなたの負けです...\n");
        }
        printf("\n");

    } while (result == 0);

}

どうでしょう?これだけで結構ソースコードが読みやすくなったのではないでしょうか?

例えば変数名が ab の場合、どちらでコンピューターが出した手を管理しているかのが分かりにくくソースコードを読んでいても書いていても混乱しがちですが、player_handcom_hand に変更することでその分かりにくさが解消されたと思います。

元々の変数名がひどすぎたこともあるのですが、改めて変数名を工夫することの重要性を理解していただけたのではないかと思います。

列挙型や定数マクロを利用する

また、ここまでじゃんけんの手を下記のように整数で表し、

  • グー:0
  • チョキ:1
  • パー:2

さらに、じゃんけんの結果を下記のように整数で表すようにしていました。

  • 引き分け:0
  • プレイヤーの勝ち:1
  • プレイヤーの負け:2

これらに関しても、どの整数が何を表しているかが直感的に分からないため、非常にソースコードが読みにくくなってしまっています。

上記のような定数に関しては、列挙型(enum)や定数マクロ(define)を利用し、定数に名前をつけてやることでソースコードを読みやすくすることができます。今回は列挙型を利用します。

列挙型に関しては下記ページで解説していますので、詳しく知りたい方はこちらを参考にしていただければと思います。

列挙型・enum の解説ページアイキャッチ【C言語】列挙型(enum)について解説

例えばじゃんけんの手であれば、下記のように列挙型の定義を行えば、HAND_ROCK には 0HAND_SCISSORS には 1HAND_PAPER には 2、さらには HAND_NUM には 3 の整数がそれぞれ割り当てられることになります。

じゃんけんの手の列挙型

enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
};

つまり、じゃんけんの手をソースコード内で記述する際には、02 といった分かりにくい整数ではなく、上記で定義した HAND_ROCK HAND_SCISSORSHAND_PAPER という意味の分かりやすい定義名で扱うことができるようになります。

これにより、整数 02 がそれぞれ何を表しているかを迷うことなくソースコードを読んだり書いたりすることができるようになります。

また、HAND_NUM にはじゃんけんの手のパターン数 3 が割り当てられることになりますので、この HAND_NUM を利用してエラーチェックや配列のサイズ定義などを行うこともできます。

さらに、下記のように typedef を利用すれば、じゃんけんの手(グー・チョキ・パー)を扱う HAND という新たな型を定義することができるようになります。

じゃんけんの手の型

typedef enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;

そして、下記のように player_handcom_hand の変数の型を HAND としてやることで、変数宣言をみるだけで、これらの変数の扱う値がじゃんけんの手である HAND_ROCK HAND_SCISSORSHAND_PAPER(さらには HAND_NUM)であることが明確に分かるようになり、これによってソースコードの可読性が向上します。

typedef に関しては下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。

typedef解説ページのアイキャッチC言語のtypedefについて具体例を用いて分かりやすく解説

同様に、「じゃんけんの結果」に関しても列挙型を定義して typedef をしてやることで、さらにソースコードの可読性を向上させることができます。

じゃんけんの結果の型

typedef enum {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

これらの列挙型を利用することで、変数名を工夫する で紹介したソースコードは下記のようになります。

列挙型を利用する

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;


typedef enum {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

int main(void) {

    HAND player_hand;
    HAND com_hand;
    RESULT result;

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

    do {
        // コンピューターが出す手を決定する
        com_hand = rand() % HAND_NUM;

        // プレイヤーが出す手の入力を受け付ける
        while (1) {
            printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
            scanf("%d", &player_hand);

            if (player_hand >= HAND_NUM || player_hand < 0) {
                printf("出した手がおかしいです...\n\n");
            } else {
                break;
            }
        }
        printf("\n");

        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        if (player_hand == HAND_ROCK) {
            printf("グー\n");
        } else if (player_hand == HAND_SCISSORS) {
            printf("チョキ\n");
        } else if (player_hand == HAND_PAPER) {
            printf("パー\n");
        }

        printf("相手が出した手 :");
        if (com_hand == HAND_ROCK) {
            printf("グー\n");
        } else if (com_hand == HAND_SCISSORS) {
            printf("チョキ\n");
        } else if (com_hand == HAND_PAPER) {
            printf("パー\n");
        }
        printf("\n");

        // じゃんけんの結果を判断する
        if (player_hand == com_hand) {
            result = RESULT_DRAW;
        } else if (
            (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
            (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
            (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
        ) {
            result = RESULT_YOU_WIN;
        } else {
            result = RESULT_YOU_LOSE;
        }

        // じゃんけんの結果を表示する
        printf("結果は・・・");
        if (result == RESULT_DRAW) {
            printf("引き分けです!!!\n");
        } else if (result == RESULT_YOU_WIN) {
            printf("あなたの勝ちです!!!\n");
        } else {
            printf("あなたの負けです...\n");
        }
        printf("\n");

    } while (result == RESULT_DRAW);

}

特に列挙型の利用によるソースコードの可読性の向上の効果が出ているのは「プレイヤーの勝ち」であるかのどうかの判断における条件式だと思います。

下記のように変更前後の条件式を見比べていただければ、変更前は何をやっているかが分かりにくいですが、変更後に関しては、下記抜粋部分を見るだけで何をしているかが直感的に理解できるのではないかと思います。

変更前

else if (
    (player_hand == 0 && com_hand == 1) ||
    (player_hand == 1 && com_hand == 2) ||
    (player_hand == 2 && com_hand == 0)
)

変更後

else if (
    (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
    (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
    (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
)

この辺りから、列挙型を利用するメリットを実感していただけるのではないかと思います。

ちなみに define を利用して定数マクロを定義し、それを利用することでも可読性を向上させることができます。が、今回のように、複数の定数が関連していることを示す際には列挙型の方がオススメです。

いずれにせよ、意味の分かりにくい整数を列挙型や定数マクロを利用して意味の分かりやすい定義名で扱えるようにすることで、ソースコードの可読性を向上させることができることは覚えておいてください。

関数化を行う

次はソースコードの構造を変更して可読性を向上させたいと思います。

現状全ての処理を main 関数で行なっているのを複数の関数に分けることで可読性の向上を試みます。

具体的には、ここまで紹介してきた下記の処理を関数として切り出して実装します。

  1. コンピューターが出す手を決定する(getComHand
  2. プレイヤーが出す手の入力を受け付ける(getPlayerHand
  3. プレイヤーとコンピューターが出した手を表示する(printHand
  4. じゃんけんの結果を判断する(getResult
  5. じゃんけんの結果を表示する(printResult

列挙型や定数マクロを利用する で紹介したソースコードに対し、上記のように複数の関数分けを行なった結果は次のようになります(初期化処理は別途 init 関数で行うようにもしています)。

複数の関数に分ける

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;

typedef enum {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

void init(void) {
    
    // 乱数の初期化
    srand((unsigned int)time(NULL));
}

HAND getComHand(void) {
    return rand() % HAND_NUM;
}

HAND getPlayerHand(void) {

    HAND player_hand;

    while (1) {
        printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
        scanf("%d", &player_hand);

        if (player_hand >= HAND_NUM || player_hand < 0) {
            printf("出した手がおかしいです...\n\n");
        } else {
            break;
        }
    }
    printf("\n");

    return player_hand;
}

RESULT getResult(HAND player_hand, HAND com_hand) {
    RESULT result;

    if (player_hand == com_hand) {
        result = RESULT_DRAW;
    } else if (
        (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
        (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
        (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
    ) {
        result = RESULT_YOU_WIN;
    } else {
        result = RESULT_YOU_LOSE;
    }

    return result;
}

void printHand(HAND hand) {
    if (hand == HAND_ROCK) {
        printf("グー\n");
    } else if (hand == HAND_SCISSORS) {
        printf("チョキ\n");
    } else if (hand == HAND_PAPER) {
        printf("パー\n");
    }
}

void printResult(RESULT result) {

    printf("結果は・・・");
    if (result == RESULT_DRAW) {
        printf("引き分けです!!!\n");
    } else if (result == RESULT_YOU_WIN) {
        printf("あなたの勝ちです!!!\n");
    } else {
        printf("あなたの負けです...\n");
    }
}

int main(void) {

    HAND player_hand;
    HAND com_hand;
    RESULT result;

    init();

    do {
        // コンピューターが出す手を決定する
        com_hand = getComHand();

        // プレイヤーが出す手の入力を受け付ける
        player_hand = getPlayerHand();
        
        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        printHand(player_hand);

        printf("相手が出した手 :");
        printHand(com_hand);

        printf("\n");

        // じゃんけんの結果を判断する
        result = getResult(player_hand, com_hand);

        // じゃんけんの結果を表示する
        printResult(result);
        printf("\n");

    } while (result == RESULT_DRAW);

}

main 関数がスッキリしましたね!

main 関数ではじゃんけんゲームの流れの制御のみを行うようにしているため、もはやコメントを消したとしても main 関数からじゃんけんゲームを実現するために行なっている処理の「大まかな流れ」が一目で理解できるようになったのではないかと思います。

また、各処理の詳細は呼び出している関数の定義を読むことで理解することができます。関数単位で各処理が分かれているため、各処理の意味合いも理解しやすくなっているのではないかと思います。

こんな感じで、関数分けを行うことはソースコードの可読性向上にもつながります。

関数分けを行うことのメリットは他にもあって、今回のソースコードの例で言えばソースコードの重複の解消が挙げられます。

例えば 列挙型や定数マクロを利用する で紹介したソースコードでは、下記のようにプレイヤーの出した手の表示とコンピューターの出した手の表示をそれぞれ別の処理として記述していました。

出した手の表示を別々の処理で実現した場合

// プレイヤーとコンピューターが出した手を表示する
printf("あなたが出した手:");
if (player_hand == HAND_ROCK) {
    printf("グー\n");
} else if (player_hand == HAND_SCISSORS) {
    printf("チョキ\n");
} else if (player_hand == HAND_PAPER) {
    printf("パー\n");
}

printf("相手が出した手 :");
if (com_hand == HAND_ROCK) {
    printf("グー\n");
} else if (com_hand == HAND_SCISSORS) {
    printf("チョキ\n");
} else if (com_hand == HAND_PAPER) {
    printf("パー\n");
}
printf("\n");

もちろんこれでも上手く動作するのですが、この場合、出した手の処理を変更する際に(例えば出した手を日本語表記ではなく英語表記にする際に)、それぞれの手に対して2箇所ずつ変更を行う必要があります。

ですが、今回のように関数化を行うことで出した手の表示は1つの処理にまとめられているため(表示する手は引数によって切り替えられる)、上記のような変更を行う場合でも、変更が必要なのはそれぞれの手に対して1箇所のみとなります。

関数化前後でのグーの文字列を変更する場合の変更箇所の違いを示した図

例えば 列挙型や定数マクロを利用する のソースコードのように処理の重複があった場合、一方の修正を忘れてしまってバグになってしまうことも良くあります。関数化を行なって処理の重複をなくすことで、そういったバグを防ぐことができ、結果的に品質の高いプログラムが実現しやすくなります。

また、関数化を行うことでソースコードの再利用性も向上させることができます。

ちょっとじゃんけんの例だと分かりにくいのでトランプゲームの例で説明すると、例えばポーカーゲームを開発した場合に「カードを配る関数」を作成しておけば、その関数をそのままババ抜きや大富豪などのトランプゲーム開発時に再利用することもできます。

ポーカーのカードを配る関数を他のトランプカードで再利用する様子

再利用を行うことで、新たに関数を作成する必要がなくなり、その分プログラミングする手間を省くことができます。もっと格好良く、ソフトウェア開発的に言えば、開発工数を削減することができます。

もちろん関数化を行わなくてもコピペすれば再利用することはできるのですが、関数として切り出されている方が再利用しやすくなります。さらに、関数テストなどが行われていれば、品質の高い関数を再利用することもできます。

こんな感じで、関数を利用することでソースコードの可読性だけでなく、品質や再利用性を向上させることも可能となります。

スポンサーリンク

テーブルを利用する

最後にテーブルを利用したソースコードの変更例を紹介しておきたいと思います。ここでいうテーブルとは、要は「参照用の配列」のことです。

関数化を行う で示したソースコードは割と読みやすくなっているのですが、特に下記のじゃんけんの結果を決める getResult 関数に関してはちょっと条件分岐が複雑です。

条件分岐で結果を決める

RESULT getResult(HAND player_hand, HAND com_hand) {
    RESULT result;

    if (player_hand == com_hand) {
        result = RESULT_DRAW;
    } else if (
        (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
        (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
        (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
    ) {
        result = RESULT_YOU_WIN;
    } else {
        result = RESULT_YOU_LOSE;
    }

    return result;
}

そこで、下図のようなテーブルを作成し、このテーブルを参照することでじゃんけんの結果を決められるようにしていきたいと思います。

じゃんけんの結果を管理するテーブルの説明図

この場合のテーブルは二次元配列となります。

この二次元配列の変数名を result_table とすれば(グローバル変数とします)、上図のようなテーブルは、例えば init 関数の中で下記のような代入を行うことで作成することができます。

テーブルの作成

void init(void) {
    // 乱数の初期化
    srand((unsigned int)time(NULL));

    // result_table[player_hand][com_hand]にプレイヤーの結果を格納
    result_table[HAND_ROCK][HAND_ROCK] = RESULT_DRAW;
    result_table[HAND_ROCK][HAND_SCISSORS] = RESULT_YOU_WIN;
    result_table[HAND_ROCK][HAND_PAPER] = RESULT_YOU_LOSE;
    result_table[HAND_SCISSORS][HAND_ROCK] = RESULT_YOU_LOSE;
    result_table[HAND_SCISSORS][HAND_SCISSORS] = RESULT_DRAW;
    result_table[HAND_SCISSORS][HAND_PAPER] = RESULT_YOU_WIN;
    result_table[HAND_PAPER][HAND_ROCK] = RESULT_YOU_WIN;
    result_table[HAND_PAPER][HAND_SCISSORS] = RESULT_YOU_LOSE;
    result_table[HAND_PAPER][HAND_PAPER] = RESULT_DRAW;
}

こういったテーブルを作成しておけば、if 文等で条件分岐を作らなくても出された手を配列の添字に指定するだけでじゃんけんの結果を得ることができるようになります。

テーブルからのじゃんけんの結果の取得例

具体的には、result_table に対して下記のように添字を指定してやることで、プレイヤーが出した手とコンピューターが出した手から結果を取得することができます。

テーブルからの結果の取得

result_table[プレイヤーが出した手][コンピューターが出した手]

つまり、先ほど掲載した getResult 関数は、引数 player_hand がプレイヤーが出した手&引数 com_hand がコンピューターが出した手なので下記のように書き換えることができます。

テーブルから結果を取得する

RESULT getResult(HAND player_hand, HAND com_hand) {
    return result_table[player_hand][com_hand];
}

条件分岐が無くなったので関数がスッキリしていることが確認することができます。もちろん result_table がどんな配列であるかを知るために init 関数を調べる手間も必要になるのですが、それでも最初の条件分岐よりも読みやすくなったのではないかと思います。

配列を用意することでメモリ使用量もその分増えるというデメリットもありますが、条件分岐が不要になった分処理速度も上がるというメリットもあります。

何でもかんでもテーブル化する必要はありませんし、まずは if 文等で条件分岐を実装し、テーブルを利用することで可読性が向上しそうであれば、テーブルの利用を検討するくらいで良いと思います。

また、じゃんけんの結果だけでなく、出した手の文字列や結果の文字列の取得もテーブルから行うようにすることで、条件分岐の削減を行うことができます。

関数化を行う で紹介したソースコードに対し、じゃんけんの結果・出した手の文字列・結果の文字列をテーブルから取得するようにした例が下記となります。

テーブルを利用する

>#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef enum _hand {
    HAND_ROCK = 0,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;

typedef enum _result {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

static RESULT result_table[HAND_NUM][HAND_NUM];
static char *hand_strs[HAND_NUM];
static char *result_strs[RESULT_NUM];

void init(void) {
    // 乱数の初期化
    srand((unsigned int)time(NULL));

    // result_table[player_hand][com_hand]にプレイヤーの結果を格納
    result_table[HAND_ROCK][HAND_ROCK] = RESULT_DRAW;
    result_table[HAND_ROCK][HAND_SCISSORS] = RESULT_YOU_WIN;
    result_table[HAND_ROCK][HAND_PAPER] = RESULT_YOU_LOSE;
    result_table[HAND_SCISSORS][HAND_ROCK] = RESULT_YOU_LOSE;
    result_table[HAND_SCISSORS][HAND_SCISSORS] = RESULT_DRAW;
    result_table[HAND_SCISSORS][HAND_PAPER] = RESULT_YOU_WIN;
    result_table[HAND_PAPER][HAND_ROCK] = RESULT_YOU_WIN;
    result_table[HAND_PAPER][HAND_SCISSORS] = RESULT_YOU_LOSE;
    result_table[HAND_PAPER][HAND_PAPER] = RESULT_DRAW;

    hand_strs[HAND_ROCK] = "グー";
    hand_strs[HAND_SCISSORS] = "チョキ";
    hand_strs[HAND_PAPER] = "パー";

    result_strs[RESULT_DRAW] = "引き分けです!!!";
    result_strs[RESULT_YOU_WIN] = "あなたの勝ちです!!!";
    result_strs[RESULT_YOU_LOSE] = "あなたの負けです...";
}

HAND getComHand(void) {
    return rand() % HAND_NUM;
}

HAND getPlayerHand(void) {

    HAND player_hand;

    while (1) {
        printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
        scanf("%d", &player_hand);

        if (player_hand >= HAND_NUM || player_hand < 0) {
            printf("出した手がおかしいです...\n\n");
        } else {
            break;
        }
    }
    printf("\n");

    return player_hand;
}

RESULT getResult(HAND player_hand, HAND com_hand) {
    return result_table[player_hand][com_hand];
}

void printHand(HAND hand) {
    printf("%s\n", hand_strs[hand]);
}

void printResult(RESULT result) {

    printf("結果は・・・");
    printf("%s\n", result_strs[result]);
}

int main(void) {

    HAND player_hand;
    HAND com_hand;
    RESULT result;

    init();

    do {
        // コンピューターが出す手を決定する
        com_hand = getComHand();

        // プレイヤーが出す手の入力を受け付ける
        player_hand = getPlayerHand();
        
        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        printHand(player_hand);

        printf("相手が出した手 :");
        printHand(com_hand);

        printf("\n");

        // じゃんけんの結果を判断する
        result = getResult(player_hand, com_hand);

        // じゃんけんの結果を表示する
        printResult(result);
        printf("\n");

    } while (result == RESULT_DRAW);

}

可読性の向上例の紹介については以上になります。

じゃんけんゲームのサンプルプログラム のソースコードと比較すれば、かなりソースコードが読みやすくなったことを実感していただけるのではないかと思います(じゃんけんゲームのサンプルプログラム のソースコードが酷すぎるというのもありますが…)。

まとめ

このページでは、C言語における「じゃんけんゲームの作り方」について解説しました!

比較的簡単なゲームですので、じゃんけんのプログラミングはC言語初心者の方が挑戦するには良い題材だと思います。

実際のゲームの流れを考え、現実世界で自然と頭の中で行なっている判断などを処理としてソースコードに記述する必要がある点がゲーム開発のポイントになると思います。

また、今回紹介したじゃんけんゲームのプログラムから、3人用のじゃんけんゲーム(プレイヤー1人・コンピューター2人)に発展させてみても面白いと思いますし、じゃんけん以外のゲームのプログラミングに挑戦してみるのも良いと思います!

また、後半ではじゃんけんゲームのプログラムのソースコードを変更することで可読性についても解説しました。

C言語プログラミングにおいて、if 文や for 文等は機能を実現する上で必須の知識となりますが、例えば enum などは知らなくても機能の実現は可能です。

ですが、enum には可読性を向上させる効果があります。そして、この可読性はチーム開発や保守性の向上に必要な指標となります。

今回示した例は初歩的な例であり、可読性を向上させるためのテクニックなどはたくさん存在します。可読性を意識してソースコードを書くようにすることでも、プログラミングの力を伸ばすことができますので、ぜひ機能の実現だけでなく、可読性の向上にも挑戦してみてください!

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