計算問題 †
これまでに勉強したことを使って、次のような簡単な計算ゲームを作ります。
- コンピューターが足し算の問題を出題する
- ユーザーがそれに答える
- 正解か不正解かをコンピューターが判定する
次のプログラムは、変数iと変数jの足し算の式を問題として出力し、入力された値が変数iと変数jの和に等しければ「正解」、そうでなければ「間違い」と出力します(プログラム1)。
#include <stdio.h> int main(void) { int i = 45, j = 76, a; printf("%d + %d = ", i, j); scanf("%d", &a); if (a == i + j) { printf("正解!\n"); } else { printf("間違い!\n"); } return 0; }
演習1 †
プログラム1を作成し、実行結果を確認せよ。
また、何回か問題を変えて実行せよ。
rand関数 †
上のプログラムは、問題がプログラムで決められているため、プログラムを変えないと問題を変えることができません。 これでは、ゲームとしては面白くありません。
そこで、乱数を使って、問題を自動的に生成します。
乱数とは、無秩序に、かつ、すべて同じ確率で出現するように並べられた数字の列(これを乱数列といいます)から取り出した数のことです。
ゲームを作るときには、乱数を良く使います。
乱数を生成するには、rand関数を使います。
rand関数は、標準ライブラリーのヘッダー・ファイル stdlib.h の中に定義されていて、このライブラリーを #include で読み込まないと使えません。
rand関数は、int型の乱数を生成します(プログラム2)。
#include <stdio.h> #include <stdlib.h> int main(void) { int r; r = rand(); printf("%d\n", r); return 0; }
演習2 †
プログラム2を作成し、何回か実行して結果を確認せよ。
srand関数 †
上のプログラムを実行すると、毎回、同じ結果になります。
rand関数は、毎回同じやり方で擬似乱数を生成するので、スタート地点が同じだと毎回同じ擬似乱数が生成されるのです。
そこで、srand関数を使って擬似乱数を生成するスタート地点(これを乱数のシードといいます)を変更します(プログラム3)。
#include <stdio.h> #include <stdlib.h> int main(void) { int r; srand(2); r = rand(); printf("%d\n", r); return 0; }
srand関数の ( ) の中の数は、擬似乱数を生成するスタート地点を表していて、これを変えると生成される乱数が変わります。
演習3 †
プログラム2をプログラム3に変更し、何回か実行して結果を確認せよ。
また、srand関数の ( ) の中の数を変えてコンパイルし直し、何回か実行して結果を確認せよ。
time関数 †
乱数のシードが同じだと生成される乱数の系列が毎回同じになってしまうので、現在の時刻を取得してこれを乱数のシードにします。
現在の時刻を取得するには、time関数を使います。 time関数を使うには、time.hという標準のライブラリーをインクルードする必要があります(プログラム4)。
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int r; srand((unsigned int) time(NULL)); r = rand(); printf("%d\n", r); return 0; }
time関数の結果をunsigned int型にキャストしてsrand関数に渡しています。 ちょっとややこしいですが、今はこういうものだと思って進めてください。
演習4 †
プログラム3をプログラム4に変更し、何回か実行して結果を確認せよ。
サイコロ †
サイコロは、1から6までの整数が同じ確率で出ます。
サイコロを、乱数で作るには、次のようにします。
- rand関数で乱数を生成する
- 生成された値を6で割ったときの余りを求める(0から5)
- 最後に、1を加える(1から6)
したがって、サイコロを振るプログラムは、次のようになります(プログラム5)。
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int r; srand((unsigned int) time(NULL)); r = rand() % 6 + 1; printf("%d\n", r); return 0; }
なお、この作り方で作った乱数は、ゲームで使うくらいならあまり問題はありませんが、科学的なシミュレーションなど精密な計算が必要なときには問題があることがわかっていて、そのような場合には他の方法で乱数を作ります。
演習5 †
プログラム4をプログラム5に変更し、何回か実行して結果を確認せよ。
計算ゲーム †
いよいよ、計算ゲームを作ります。
計算ゲームの流れは以下のようになります。
- コンピューターが足し算の問題を作り、出題する
- ユーザーがそれに答える
- 正解か不正解かをコンピューターが判定する
これをもう少しC言語風にします。
- rand関数を使って整数同士の足し算の問題を乱数を使って作成する
- scanf関数を使ってキーボードから入力された整数を読み取る
- if文を使って入力された答えが正しいかどうかを判定し、printf関数を使ってメッセージを出力する
したがって、計算ゲームのプログラムは、次のようになります(プログラム6)。
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int i, j, a; srand((unsigned int) time(NULL)); i = rand() % 99 + 1; j = rand() % 99 + 1; printf("%d + %d = \n", i, j); scanf("%d", &a); if (a == i + j) { printf("正解!\n"); } else { printf("間違い!\n"); } return 0; }
演習6 †
プログラム6を作成し、何回か実行して結果を確認せよ。
もうすこしマシなサイコロ(おまけ) †
上の、剰余を使ったサイコロでは、生成された(2進数の)擬似乱数の下位ビットを使って乱数を作っています。 (10進数の擬似乱数に対して0から9までのサイコロを作るために擬似乱数を10で割ったときの余りを使うと、1の位だけを使っていることになるのと同じです。)
ところが、rand関数が生成する擬似乱数の下位ビットは乱数としてあまり良くない(周期性がある)ことがわかっています。 そこで、生成された擬似乱数の上位ビットを使って乱数を作ります。
rand関数が生成する最も小さな数は0、最も大きな数はstdlib.hの中で RAND_MAX として定義されています。
これを使って1から6までの乱数を生成するには、次のようにします。
- 生成した乱数を RAND_MAX + 1.0 で割って0以上1未満の実数(0から0.999999999...)を作る
- これを6倍して0以上6未満の実数(0から5.999999999...)にする
- その整数部分(0から5)を取り出す
- 最後に、1を加える(1から6)
したがって、剰余を用いて取り出した乱数の下位ビットによるサイコロよりもすこしマシなサイコロを振るプログラムは、次のようになります(プログラム7)。
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { int r; srand((unsigned int) time(NULL)); r = (int) (rand() / (RAND_MAX + 1.0) * 6) + 1; printf("%d\n", r); return 0; }
ちなみに、この作り方でもまだ問題があることがわかっています。 また、rand関数そのものにも問題があるので、きちんとした乱数が必要なときにはrand関数を使ってはいけません。 (メルセンヌ・ツイスターという方法を使います。)
演習7 †
プログラム5をプログラム7に変更し、何回か実行して結果を確認せよ。
まとめ †
これまでに勉強したif文、画面への出力、キーボードからの入力を組み合わせると、画面にメッセージを表示してキーボードから何かを入力してもらい、その入力に応じて画面に出力するメッセージを変えるといったことができます。
今回は、これを利用して計算ゲームを作りました。
ただし、毎回同じ問題ではゲームとしてつまらないので、乱数を用いて毎回違う問題が出題されるようにしました。 乱数の生成に使われるrand関数、srand関数、time関数、unsigned int型、NULL、RAND_MAXについては、今の時点では理解できなくても構いません。