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

単語当てゲームLv2の実装(C/C++)


今回は、 第74回 で作った単語当てゲームLv1をLv2の仕様に合わせて改造していきます。

まずはLv1のコードを再掲載します。
<  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>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main(void){
   
//ソース上に直接書き込んだ問題
   
char question[10][2][16]={
      {
"1+2=","3"},
      {
"3+4=","7"},
      {
"1-2=","-1"},
      {
"3-4=","-1"},
      {
"1*2=","2"},
      {
"3*4=","12"},
      {
"1/2=","0.5"},
      {
"3/4=","0.75"},
      {
"1の2乗=","1"},
      {
"3の4乗=","81"}
   };
   
//選択された問題
   
int sel_question;
   
//入力された答え
   
char player_ans[80]="";
   
   
//機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   printf(
"%s",question[sel_question][0]);
   
   
while(1){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
   
      
//機能「成否を判定します」
      
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;

まずは、問題を変更しておきます。
問題は英数文字と日本語が適当に混在するようにしておきます。
実行時に日本語文字を入力するにはAlt+半角/全角キーでできるようになるはずです。
<  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>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main(void){
   
//ソース上に直接書き込んだ問題
   
char question[10][2][64]={//日本語の問題を書くには16では足りないので大きくします
      {"「プログラム」を英語で書くと?","program"},
      {
"日本で一番高い山は?","富士山"},
      {
"3*5=","15"},
      {
"「library」をカタカナ読みすると?","ライブラリ"},
      {
"日本の都道府県の数は?","47"},
      {
"「檸檬」はなんて読む?","れもん"},
      {
"「万」の上の単位は?","億"},
      {
"「迎撃」はなんて読む?","げいげき"},
      {
"22+33*5=","187"},
      {
"「メモリ」を英語で書くと?","memory"}
   };
   
//選択された問題
   
int sel_question;
   
//入力された答え
   
char player_ans[80]="";
   
   
//機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   printf(
"%s",question[sel_question][0]);
   
   
while(1){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
   
      
//機能「成否を判定します」
      
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;

続いて、追加された要素を置く場所を決めます。
前回用意した図の通りに配置します。
<  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>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

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]="";
   
//ヒント文字列
   
   //機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   printf(
"%s",question[sel_question][0]);
   
   
while(1){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「ヒント文字列の表示」
      
      //機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
   
      //機能「成否を判定します」
      
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;

ヒント文字列用の変数を定義し、
「乱数を使って選択し表示します」での初期化を追加します。
また、前々回に用意した日本語判定関数を定義します。
<  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>
#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]="";//(1)とりあえず入力された答えと同じだけ確保しておきます。
   //ループカウンタ用一時変数
   
int i;
   
   
//機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   printf(
"%s",question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){//(2)答えの文字列の終端まで回します
      
if(SJISMultiCheck(question[sel_question][1][i])){//(3)日本語文字か確認
         
strcat(hint_str,"*");//(4)*をヒント文字列に追加
         
i+=2;//全角一文字分ずらします
      }
      
else{
         strcat(hint_str,
"*");//(5)*をヒント文字列に追加
         
i++;//半角一文字分ずらします
      }
   }

   
while(1){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「ヒント文字列の表示」
      
      //機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
   
      //機能「成否を判定します」
      
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;
(1)30行を「入力された答え」と同じにしておいたのは、
「ヒント文字列の更新」処理の時に「入力された答え」と比較することになるので、
こうしておいた方が楽できるかと思うからです。
(後に正解文字列の最大数で問題ないことが判明しますが、今はこのままで)

(2)38行は、選択した問題の正解文字列の最後のナル文字まで回します。

(3)39行は、見ている文字が全角文字の第1バイトかを調べます。
   全角文字の場合は全角の*を、半角文字の場合は半角の*をヒント文字列に加えます。

(4)40行、(5)44行は、それぞれ hint_str に*と*の文字列を追加します。
strcat 関数は第一引数に指定したナル終端文字列に第二引数に指定したナル終端文字列を連結します。
そのため、第一引数がナル終端文字列ではなかったり、空き容量が足りないと
バグりますので注意してください。
strcat関数は毎回終端を探すため、strcpy関数を上手く使った方が高速ですが
strcat関数の方がわかりやすいのでこちらを使いました。



ヒントが問題の後に続いてしまうと見づらいので、
問題を表示したら改行するようにしておきます。
ヒント文字列は printf 関数でそのまま表示してしまいます。
<  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>
#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;
   
   
//機能「乱数を使って選択し表示します」
   
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){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「ヒント文字列の表示」
      
printf("ヒント:%s\n",hint_str);
      
      
//機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
   
      //機能「成否を判定します」
      
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;

さて、今回の中核とも呼べるのが次の「ヒント文字列の更新」機能です。
よくわからないからここまで素通りで見てきたという人は、
すぐにこの先を読まずに上の段階のソースを元にして
この機能だけでも考えてみてください。(一時変数は追加します)


それでは、「ヒント文字列の更新」機能を実装します。
<  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>
#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;
   
//正解文字列の長さ用一時変数
   
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){//for(;;)が本来の無限ループですが、こっちもよく使われます
      //機能「ヒント文字列の表示」
      
printf("ヒント:%s\n",hint_str);
      
      
//機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
      
ans_len=strlen(question[sel_question][1]);//(1)正解文字列の長さを取得します
      
player_len=strlen(player_ans);//(2)プレイヤーの回答文字列の長さを取得します
      
for(i=0;(i<ans_len)&&(i<player_len);){//(3)回答と正解文字列を比較していきます
         
if(SJISMultiCheck(player_ans[i])){//日本語文字の場合
            
if((player_ans[i]==question[sel_question][1][i])&&
               (player_ans[i+1]==question[sel_question][1][i+1])){
//(4)2バイトの一致を調べ
               
hint_str[i]=question[sel_question][1][i];//(5)2バイト単位で処理する
               
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;
(1)61行目は、正解文字列の長さを取得し、
(2)62行目でプレイヤーが入力した文字列の長さを取得しています。

(3)63行目の継続条件は、「今からチェックする文字の位置」が
正解文字列でもプレイヤーの入力文字列でも有効であることを確認します。
どちらか一方でも文字列の範囲外ならば、その場所を検証しても意味がない(というかバグる可能性もある)ので、
両方が文字列範囲内であることを継続条件とします。

(4)65〜66行目は、「全角文字を検証する場合は2バイト単位で」という話です。
1バイトづつバラで判定すると第一バイトだけ、第二バイトだけヒットして文字化けが起きる原因になります。
(5)67〜68行目は正解文字列の全角文字をヒント文字列にコピーしています。



さて、これで一応Lv2の仕様で動いている・・・ように見えます。
しかし、「ヒント文字列の更新」機能内にはバグがあり、特定の入力を行うとヒント文字列が化けます。
作っている時点でこの問題には気付いたのですが、これはこれまで この講座で解説した内容だけでは
発見も、回避も難しいような問題だったため、今回の解答例では対応しませんでした。
この問題は次回で詳しく対応しようと思います。
(特定の入力を運で見つけ出すのはかなり難しいと思いますので、どうなるのか気になる人は、
「日本で一番高い山」の問題が出た時にxxmmRRとか回答してみてください。発現します)

また、今回のコードは出来としては微妙なところがあります。
これを考えなおしてより良いコードにする作業は次々回でやる予定です。

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

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

最終更新 2010/04/02