[前へ]   [目次へ]   [次へ]

単語当てゲームLv2の改良(1)(C/C++)


今回は、前回までに作った単語当てゲームのソースを、
見やすく、読みやすいように改良していきます。

まず前回までのソースコードを再掲載します。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
< 55>
< 56>
< 57>
< 58>
< 59>
< 60>
< 61>
< 62>
< 63>
< 64>
< 65>
< 66>
< 67>
< 68>
< 69>
< 70>
< 71>
< 72>
< 73>
< 74>
< 75>
< 76>
< 77>
< 78>
< 79>
< 80>
< 81>
< 82>
< 83>
< 84>
< 85>
< 86>
< 87>
< 88>
< 89>
< 90>
< 91>
< 92>
< 93>
< 94>
< 95>
< 96>
< 97>
< 98>
< 99>
<100>
<101>
<102>
<103>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int SJISMultiCheck(unsigned char c){
   
if(((c>=0x81)&&(c<=0x9f))||((c>=0xe0)&&(c<=0xfc)))return 1;
   
else return 0;
}

int main(void){
   
//ソース上に直接書き込んだ問題
   
char question[10][2][64]={
      {
"「プログラム」を英語で書くと?","program"},
      {
"日本で一番高い山は?","富士山"},
      {
"3*5=","15"},
      {
"「library」をカタカナ読みすると?","ライブラリ"},
      {
"日本の都道府県の数は?","47"},
      {
"「檸檬」はなんて読む?","れもん"},
      {
"「万」の上の単位は?","億"},
      {
"「迎撃」はなんて読む?","げいげき"},
      {
"22+33*5=","187"},
      {
"「メモリ」を英語で書くと?","memory"}
   };
   
//選択された問題
   
int sel_question;
   
//入力された答え
   
char player_ans[80]="";
   
//ヒント文字列
   
char hint_str[80]="";
   
//ループカウンタ用一時変数
   
int i,j;
   
//正解文字列の長さ用一時変数
   
int ans_len;
   
//入力文字列の長さ用一時変数
   
int player_len;
   
   
//機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint_str,
"*");
         i+=2;
      }
      
else{
         strcat(hint_str,
"*");
         i++;
      }
   }

   
while(1){
      
//機能「ヒント文字列の表示」
      
printf("ヒント:%s\n",hint_str);
      
      
//機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
      
ans_len=strlen(question[sel_question][1]);
      player_len=strlen(player_ans);
      j=0;
      
for(i=0;(i<ans_len)&&(i<player_len);){
         
while(j<i){
            
if(SJISMultiCheck(question[sel_question][1][j]))j+=2;
            
else j++;
         }
         
if(i!=j){
            
if(SJISMultiCheck(player_ans[i]))i+=2;
            
else i++;
         }
         
else{
            
if(SJISMultiCheck(player_ans[i])){//日本語文字の場合
               
if((player_ans[i]==question[sel_question][1][i])&&
                  (player_ans[i+1]==question[sel_question][1][i+1])){
                  hint_str[i]=question[sel_question][1][i];
                  hint_str[i+1]=question[sel_question][1][i+1];
               }
               i+=2;
            }
            
else{//半角文字の場合
               
if(player_ans[i]==question[sel_question][1][i]){
                  hint_str[i]=question[sel_question][1][i];
               }
               i++;
            }
         }
      }
   
      
//機能「成否を判定します」
      
if(player_ans[0]!='\0')player_ans[strlen(player_ans)-1]='\0';
      
if(!strcmp(player_ans,question[sel_question][1]))break;
      puts(
"違います。もう一度入力してください。");
   }
   
   
//機能「クリア表示して終了」
   
puts("正解です。Enterを押すと終了します。");
   
   
//終了待ち
   
getchar();
   
return 0;

前回のソースは本講座初の100行超えでした。
main 関数の長さも93行と結構な長さとなってきました。
今回はこれを関数に切り出して整理していきます。

ここでまた重要になってくるのが以前作った関係図です。
Lv2の関係図
実は元々この関係図は関数に切り出す時に必要な情報を記述したものです。

さて、それでは始めます。
まずは各機能のコード量を見てみます。
機能名行数
乱数を使って選択し表示します13
ヒント文字列の表示1
その問題の答えをプレイヤーはキーボードから入力1
ヒント文字列の更新29
成否を判定します3
クリア表示して終了1

こうして見ると、「乱数を使って選択し表示します」と
「ヒント文字列の更新」がやたらと大きいことが分かります。
なのでこの二個の機能を関数化していくことにします。


まず「乱数を使って選択し表示します」機能を関数化します。

この機能の本質は「問題を選ぶこと」なので、関数名は
SelectQuestion としておくことにします。

? SelectQuestion()

先の関係図からこの機能がアクセスする要素は以下のようになっています。
●[入力]ソース上に直接書き込んだ問題
●[作成]選択された問題
●[作成]ヒント文字列
つまり、少なくともこれら三個の情報をやり取りする必要があるわけです。

まず、[入力]ソース上に直接書き込んだ問題ですが、
これは「問題」要素の複数形で、問題の集合体です。

そこで、この要素は配列へのポインタとして受け取ることにします。
main 関数にある question をそのまま受け取れるように型は const char(*)[2][64] として、
要素数を受け取る引数とセットで追加します。
何故か呼び出し時にVC6では char[10][2][64] から const char(*)[2][64]に変換できないとかエラーになりました・・・
VC2008だと普通にコンパイルできました。・・・VC6のコンパイラのバグですか?


? SelectQuestion(const char (*question)[2][64],size_t question_cnt)

ここで出てきた size_t 型ですが、大きさを表す型ということになっていて、
C標準ライブラリで要素数などを要求する場合などに使われる符号なし整数型です。
その実態は大抵 unsigned int なので、あまり特別なことは考えなくても大丈夫です。

次に、[作成]選択された問題ですが、選択した番号を返せばいいだけなので、
これは戻り値にしてしまいます。

int SelectQuestion(const char (*question)[2][64],size_t question_cnt)

最後に、[作成]ヒント文字列です。
この文字列領域に**を埋める必要があるので、書き込み可能な char* 型で受け取り、
要素数を受け取る引数とセットで追加します。

int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt)

これで、新しく作る関数の仕様が完成しました。
続いて、中身を入れていきます。
まずは、今まで main 関数にあったコードを移動させてきます。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:選択された問題のID
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
srand(time(NULL));
   sel_question=rand()%10;
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint_str,
"*");
         i+=2;
      }
      
else{
         strcat(hint_str,
"*");
         i++;
      }
   }

さて、ここからこのコードを関数として成り立つように修正していきます。
まず12行目にある srand(time(NULL)); ですが、
これは初期化処理なので main 関数でやることにして、こちらからは消しておきます。
こうすることによって、この関数を何度も使用する時に乱数の再初期化が行われるのを防ぎます。

続いて、 hint_str を hint と変数名を改めておきます。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:選択された問題のID
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
sel_question=rand()%10;
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint,
"*");
         i+=2;
      }
      
else{
         strcat(hint,
"*");
         i++;
      }
   }

続いて、 sel_question と i を定義しなおし、
問題を選択する時に生成する乱数の値域を0〜問題セットの要素数-1までに修正します。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:選択された問題のID
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
int i,sel_question;
   sel_question=rand()%question_cnt;
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint,
"*");
         i+=2;
      }
      
else{
         strcat(hint,
"*");
         i++;
      }
   }

次に、 hint の準備をします。
引数で受け取った領域に出力する場合、
事前に呼び出し元が初期化されていなくても問題ないようにしておくべきです。

というのも、関数の中身を呼び出し元が知らない場合もありますし、
知っていても呼び出し元と関数の記述位置が遠いとソース上で分離して見えます。
出力するだけの変数なら呼び出し側の感覚では初期化する必要性をあまり感じないので、
特定の値が入っていることを要求することはできるだけ避けるようにします。

ということで、準備として必要なことは hint の中身をクリアすることと、
生成するヒント文字列が hint に収まるかどうかを確認しておくことです。

hint のクリアは strcpy 関数で空文字列で代入してもいいですが、
単純に hint[0] に '\0' を代入するだけで十分です。

文字列が収まるかどうかについては、生成する文字列は選択した問題の答えと同じ長さなので、
hint の有効サイズである hint_cnt と選択した問題の答えの長さを比較すればOKです。

さて、ここで長さが足りないことがわかったら処理を完成させることができません。
この時は-1を返して失敗を意味することにしておくことにします。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:0以上:選択された問題のID
           -1:失敗
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
int i,sel_question;
   sel_question=rand()%question_cnt;
   
if(hint_cnt<=strlen(question[sel_question][1]))return -1;
   hint[0]=
'\0';
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint,
"*");
         i+=2;
      }
      
else{
         strcat(hint,
"*");
         i++;
      }
   }

最後に、選択した問題のIDを return して完成です。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:0以上:選択された問題のID
           -1:失敗
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
int i,sel_question;
   sel_question=rand()%question_cnt;
   
if(hint_cnt<=strlen(question[sel_question][1]))return -1;
   hint[0]=
'\0';
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint,
"*");
         i+=2;
      }
      
else{
         strcat(hint,
"*");
         i++;
      }
   }
   
return sel_question;


ここでいったん、ここまで行ったソースの全体を掲載します。
元のバージョンよりも全長が長くなっていますが、
main 関数が縮んで少し読みやすくなったと思います。
なお、 main 関数では SelectQuestion 関数が成功したかをチェックしていません。
チェックの仕方は少し考えれば分かると思うので、追加してみるのもいいでしょう。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
< 55>
< 56>
< 57>
< 58>
< 59>
< 60>
< 61>
< 62>
< 63>
< 64>
< 65>
< 66>
< 67>
< 68>
< 69>
< 70>
< 71>
< 72>
< 73>
< 74>
< 75>
< 76>
< 77>
< 78>
< 79>
< 80>
< 81>
< 82>
< 83>
< 84>
< 85>
< 86>
< 87>
< 88>
< 89>
< 90>
< 91>
< 92>
< 93>
< 94>
< 95>
< 96>
< 97>
< 98>
< 99>
<100>
<101>
<102>
<103>
<104>
<105>
<106>
<107>
<108>
<109>
<110>
<111>
<112>
<113>
<114>
<115>
<116>
<117>
<118>
<119>
<120>
<121>
<122>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int SJISMultiCheck(unsigned char c){
   
if(((c>=0x81)&&(c<=0x9f))||((c>=0xe0)&&(c<=0xfc)))return 1;
   
else return 0;
}

int SelectQuestion(const char (*question)[2][64],size_t question_cnt,char *hint,size_t hint_cnt){
   
/********************************************
   乱数を使って問題を選択して表示する
   
   戻り値:0以上:選択された問題のID
           -1:失敗
   
   const char (*question)[2][64]:選択対象の問題セット。
   size_t question_cnt:questionの要素数
   char *hint:[出力]ヒント文字列を格納する領域
   size_t hint_cnt:hintの要素数
   ********************************************/
   
int i,sel_question;
   sel_question=rand()%question_cnt;
   
if(hint_cnt<=strlen(question[sel_question][1]))return -1;
   hint[0]=
'\0';
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint,
"*");
         i+=2;
      }
      
else{
         strcat(hint,
"*");
         i++;
      }
   }
   
return sel_question;
}

int main(void){
   
//ソース上に直接書き込んだ問題
   
char question[10][2][64]={
      {
"「プログラム」を英語で書くと?","program"},
      {
"日本で一番高い山は?","富士山"},
      {
"3*5=","15"},
      {
"「library」をカタカナ読みすると?","ライブラリ"},
      {
"日本の都道府県の数は?","47"},
      {
"「檸檬」はなんて読む?","れもん"},
      {
"「万」の上の単位は?","億"},
      {
"「迎撃」はなんて読む?","げいげき"},
      {
"22+33*5=","187"},
      {
"「メモリ」を英語で書くと?","memory"}
   };
   
//選択された問題
   
int sel_question;
   
//入力された答え
   
char player_ans[80]="";
   
//ヒント文字列
   
char hint_str[80]="";
   
//ループカウンタ用一時変数
   
int i,j;
   
//正解文字列の長さ用一時変数
   
int ans_len;
   
//入力文字列の長さ用一時変数
   
int player_len;
   
   srand(time(NULL));
   
//機能「乱数を使って選択し表示します」
   
sel_question=SelectQuestion(question,10,hint_str,80);

   
while(1){
      
//機能「ヒント文字列の表示」
      
printf("ヒント:%s\n",hint_str);
      
      
//機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
      
ans_len=strlen(question[sel_question][1]);
      player_len=strlen(player_ans);
      j=0;
      
for(i=0;(i<ans_len)&&(i<player_len);){
         
while(j<i){
            
if(SJISMultiCheck(question[sel_question][1][j]))j+=2;
            
else j++;
         }
         
if(i!=j){
            
if(SJISMultiCheck(player_ans[i]))i+=2;
            
else i++;
         }
         
else{
            
if(SJISMultiCheck(player_ans[i])){//日本語文字の場合
               
if((player_ans[i]==question[sel_question][1][i])&&
                  (player_ans[i+1]==question[sel_question][1][i+1])){
                  hint_str[i]=question[sel_question][1][i];
                  hint_str[i+1]=question[sel_question][1][i+1];
               }
               i+=2;
            }
            
else{//半角文字の場合
               
if(player_ans[i]==question[sel_question][1][i]){
                  hint_str[i]=question[sel_question][1][i];
               }
               i++;
            }
         }
      }
   
      
//機能「成否を判定します」
      
if(player_ans[0]!='\0')player_ans[strlen(player_ans)-1]='\0';
      
if(!strcmp(player_ans,question[sel_question][1]))break;
      puts(
"違います。もう一度入力してください。");
   }
   
   
//機能「クリア表示して終了」
   
puts("正解です。Enterを押すと終了します。");
   
   
//終了待ち
   
getchar();
   
return 0;

大分長くなってきた (主にソースが長いせいで) のでここでいったん切ります。
次回は「ヒント文字列の更新」機能の関数化を行う予定です。
基本的にやり方は今回と同様なので、
次回を読む前に自分でやってみると経験値が得られるかと思います。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2010/06/28