『楽しく!』『できる!!』C講座のページ


2002.12.5
 講座内容で、この色で示している <stdio.h> <stdlib.h> の部分がタグと誤認されて消えてしまっていました。'<' '>' を用いていた場所を '<' '>' に修正しました。

第6回:乱数をつかって本当のゲームを作ろう!

・乱数
さて、前回は条件分岐の別の形switch文をつかってソースを改良(?)してみました。
 今回はいよいよお待ちかね乱数を使います。

・乱数
乱数という言葉を聞いたことがあるでしょうか?最近はゲームをやっている人、コンピュータを触っている人なら聞いたことがあるかもしれません。
乱数(random number) とは,何の規則性もなくデタラメに発生する数のことです。


-----以下、なんとなく書いてみました。読み飛ばしてください(笑)-----

この乱数を計算によって発生させるためのアルゴリズム(方法)も色々と研究されています。ただし,計算によって乱数を発生させようとすると、最初のうちはいいのですが、やがて同じ数が現れて繰り返しが生じてしまうという宿命があります。そのため、研究のポイントは繰り返し周期の長い乱数列の生み出し方にあるようです。
とはいえ、繰り返しのサイクルが十分に大きければ、実用上乱数とみなすことができます。計算によって求められる乱数は、本当は乱数もどきなので、擬似乱数(pseudo-random number)と呼ばれます。
乱数を用いて行う数学的な計算の技法はしばしば「モンテカルロ法」と呼ばれます。この名前はカジノで有名な街・モンテカルロに由来しています。モンテカルロ法では、大量の乱数を発生させて計算機の中で繰り返し実験を行い、多数回の実験結果から何らかの普遍的な共通因子を抽出します。
コンピュータの応用分野であるシミュレーション問題のうち、ランダムな要素を含むものは全てモンテカルロ法の対象となります。確率とは本来無関係な問題であっても、問題となる方程式などの構造を調べることにより、それがある確率事象を表していることを見いだせば、モンテカルロ法で数値的に解くことができます。
急に専門的な説明を始めたのでびっくりしましたか?

-----以上、なんとなく詳しい説明です。-----


まぁ、上のことは独学で勉強しようとすると、出てくるかもしれない用語だったので説明したまでです。「へぇ、そうなんだ。」ぐらいに頭の隅に置いておいてくれればOKです。
難しいことを並べましたが、なんといってもゲームでお世話になっている人が一番多いのではないでしょうか?
今回は乱数を利用してじゃんけんゲームにゲーム的な要素を与えたいと思います。

・ 乱数
今回使う乱数の書式は

 #include<stdlib.h>
:
 void srand(unsigned int seed); 
 n = rand();

となります。
なにやら、また訳の分からない言葉が出てきています。
まず、一行目の#include <stdlib.h>というのは、このプログラムで乱数を作り出すsrandとrandを使う為のおまじないです。
 続いて、void 〜;という部分ですが、この srandによって、以下で呼ばれるrand()という関数が返す値の初期化をします。

voidというのは「無」ということで何も値を返さないということです。つまり、ここではsrandは何も値も返さないということです。
 ここで勘の良い人は気付いたかもしれません。プログラムのmainの前にあるvoid。これをおまじない。といってきましたが、これも実は何も値を返さない。ということを意味しています。
 mainが何か値を返すのか?と疑問に思うかもしれませんが、プログラムの正常に終了したかどうか?などをコレを利用すれば判定できます。
 まぁ、この程度のプログラムならそこまでしなくても大丈夫でしょうから、今回はvoidにしています。

unsignedはその値が常に0かそれ以上であることを意味します。つまり、seedは整数型で0以上の数ということです。
その一行下のrand()は乱数を返す関数ですから、nに乱数発生で求められた値が代入される訳です。
整理しましょう。
乱数を使うには、stdlib.hというファイルを使用し、srandとrandいう関数によってもとめる。
このとき、srandによってrandで返す乱数を初期化する。(srandによる初期化を乱数ジェネレータの初期化と言います。)
乱数ジェネレーターを初期化したのち、randによって乱数を求めます。
簡単ですね。
では、簡単なプログラムを作ってみましょう。

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

 void main()
 {
 double n;
 srand(3);
 n = rand();
 printf("%f\n",n); 
 }

どうですか?一度も今回のソースに出てきていない、でたらめな値、つまり乱数が出てきましたね?
それでは、もう一度同じプログラムを実行してみてください。
どうでしょう?気づきましたか?そうです。このプログラムでは、何度実行しても同じ値しか返ってこないのです。
こんな乱数では利用しても前回と変わりませんね。(値が決まってるのですから。)
もう一度上の説明を思い出してください。乱数を求めているのはrand()でしたが、その前にsrand(unsigned int seed)で初期化しているのでした。ここでは、seed(種)を3で初期化しています。
「?」
気がつきましたか?そうです。初期化している値が3という一定の値のため、3をもとに生成された(実際に3を利用しているわけではないが)乱数を出力しているのです。 そうと分かれば、こっちのものですね。さっそくsrand(3)をsrand(5)に変えて実行してみましょう。
どうです?値が変わったでしょう。
つまり、srand()の()内の値を変化させれば本当の乱数に近い疑似乱数を生成できるのです。
しかし、srandの中の値が常に一定のとき、(一定なのだから乱数という表現はおかしいが)一定の乱数を返してしまうのでは、srand()の中の値も変化させなければなりません。
…。どうしましょう。乱数を使うために乱数が必要になってしまいました。これでは本当にランダムな値を生み出すことはできません。
しかぁし!これを解決する方法がきちんとあるのです。
(↑わざとらしいですね(^^;)
"常に同じ値を持たないもの"がこの世には存在します。そうです。時間です。 このsrand()の中の値を時間をもとに求めれば、不規則な値を求めることができるのです。(時間に多少左右されるため完全な乱数とは言えませんが。)

では、先ほどのソースを書き換えてみましょう。

 #include <stdio.h>
 #include <stdlib.h>
 #include <time.h> //時間を求める関数timeを使うために必要 

 void main()
 {
 double n,m;
 m=(unsigned)time(NULL);
 srand(m);
 n = rand();
 printf("%f\n",n);
 }

と変更して、同じように実行して見てください。
さっきと異なり、毎回異なった乱数が表示されたと思います。
ここで、time(NULL)によって、時間をもとにした値がmに代入され、そのmをもとにsrand(m)で乱数ジェネレーター初期化しているわけです。
理解を助けるために、一度 m という変数に値を格納した後、srand(m)という形で使用していますが、一般的にはmをそのままsrandのなかに入れてしまう

 srand((unsigned)time(NULL)) 

という形のほうが一般的です。
どちらでも、同じですので自分にわかりやすいように書いてください。
しかし、まだ問題点があります。確かに欲しいのはでたらめな値ですが、そのでたらめな値をもとに判定します。
判定に使う値なのに、あるときは1またあるときは29473932などと返されたのでは、x>=yという判定はできませんね。
そこで用いるのが モジュロ演算子 % です。
これは、あまりを求める演算子で、21%4とすれば、その値は1になります。以下にいくつかの例を示します。
x = 24%5 x = 27%3 x = 25%34
としたとき、xの値は左から4, 0, 25となります。
 ここで、ある宇宙の法則を思い出してください。
そう、割り算のあまりは割る数より小さいという法則を…。(笑)
ま、当たり前といわれればそれまでなのですが、これを用いて乱数を制御するのです。
つまり、x=乱数%3 とすれば xは0から2の値が与えられます。
では、x=乱数%5ではどうでしょう?
そうです。xは0〜4になりますね。
これをプログラムにすると、
x = rand()%5;
となり、このときxには0〜4の値が与えられるわけです。
これで0〜n(nはある自然数)をランダムに求める方法が理解できたと思います。
それでは、1〜6までの値をランダムに求めたいときはどうしますか?
そうです。
x = rand()%6+1;
とすれば、OKですね?
これで、任意の範囲の整数をランダムに求めることができます。

・ 乱数を使って…
それではお待ちかね今回のメインイベントです。
乱数を利用したじゃんけんプログラムをつくろうです。
ソースは以下のようになります。




今回のソース


 /*Janken ようやく普通のじゃんけんゲームになった */

 #include <stdio.h>
 #include <stdlib.h>
   /* 乱数関数 srand(), rand() を使うために必要 */ 
 #include <time.h>   /* 時間を求めるtime()を使うために必要 */
 void main(void)
 {
   char selected;    /* プレイヤーが選択したものを入れておく */
   int com;        /* コンピュータの出す手を入れておく */

   printf("************************************** \n");
   printf("*        じゃんけんゲーム        * \n");
   printf("************************************** \n");
   printf("\n");
   printf("#### 説明 ########\n");
   printf("# g : グー      #\n");
   printf("# t : チョキ     #\n");
   printf("# p : パー     #\n");
   printf("#################\n");
   printf("\n");
   printf("\n");
   printf("ジャンケンしよう!\n");
   printf("ジャンケン……\n");
   scanf("%c",&selected);     /* プレイヤーが選択したキャラを
                        selected に格納         */
   printf("ほいっ!!\n");

   srand((unsigned)time(NULL));    /* 乱数ジェネレーターの初期化 */
   com = rand()%3+1;           /* コンピュータの出す手を
                            1から3で決める        */ 

/* com の値を出すキャラクターに置き換える */

   switch(com){
   case 1:
        com = 'g';
        break;
   case 2:
        com = 't';
        break;
   case 3:
        com = 'p';
        break;
   }

   switch(selected){
   case 'g':
        if(selected == com){
          printf("++++++++++++++\n");
          printf(" あなた:グー\n");
          printf(" わたし:グー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あいこ! #\n");
          printf("\n");
        }
        else if(com=='t'){
          printf("++++++++++++++\n");
          printf(" あなた:グー\n");
          printf(" わたし:チョキ\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あなたの勝ち #\n");
          printf("\n");
        }
        else if(com=='p'){
          printf("++++++++++++++\n");
          printf(" あなた:グー\n");
          printf(" わたし:パー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# わたしの勝ち #\n");
          printf("\n");
        }

        break;


   case 't':
        if(selected == com){
          printf("++++++++++++++\n");
          printf(" あなた:チョキ\n");
          printf(" わたし:チョキ\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あいこ! #\n");
          printf("\n");
        }
        else if(com=='g'){
          printf("++++++++++++++\n");
          printf(" あなた:チョキ\n");
          printf(" わたし:グー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# わたしの勝ち #\n");
          printf("\n");
        }
        else if(com=='p'){
          printf("++++++++++++++\n");
          printf(" あなた:チョキ\n");
          printf(" わたし:パー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あなたの勝ち #\n");
          printf("\n");
        }
        break;

   case 'p':
        if(selected == com){
          printf("++++++++++++++\n");
          printf(" あなた:パー\n");
          printf(" わたし:パー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あいこ! #\n");
          printf("\n");
        }
        else if(com=='t'){
          printf("++++++++++++++\n");
          printf(" あなた:パー\n");
          printf(" わたし:チョキ\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# わたしの勝ち #\n");
          printf("\n");
        }
        else if(com=='g'){
          printf("++++++++++++++\n");
          printf(" あなた:パー\n");
          printf(" わたし:グー\n");
          printf("++++++++++++++\n");
          printf("\n");
          printf("# あなたの勝ち #\n");
          printf("\n");
        }

        break;


    default:
        printf("エラー!!");
        printf("g,t,p の中から選択してください!!\n");

        break;

    }
 }

です。
今回は、ようやくじゃんけんゲームが完成しました。
乱数を使うことでじゃんけんのシステムを実現したわけです。
しかし、あいこでもゲームが終了してしまうなど、本当のじゃんけんゲームまでは、まだ少しかかるようです。
次回には 繰り返し文 を利用して、あいこの時はもう一度じゃんけんを行うようにしたいと思います。
次回やってもらう繰り返し文をマスターすればプログラミングの基本である2つの大きな柱をマスターしたことになります。