こんにちは、コン(@pippi_kon)です。
この記事では『C言語でのクイズゲームの作り方』をご紹介しています。
前回は、スコア(解答結果)の記録と表示をする方法をご紹介しました。

クイズが終わった後に結果発表すると、なんかそれっぽくなりますよね!
今回は、よりクイズゲームっぽくさせるために、問題をランダム(シャッフル)で出題する方法をご紹介します。

今回の目標
今回作成するプログラムの出力結果です。

前回までと出題される順番が違っていることにお気づきでしょうか?
今回のプログラムでは、実行するたびに出題される順番が変わります。
やっていることは単純で、以下をクイズ数分繰り返しています。
- 乱数(0 or 1 or 2)を生成する
 - 乱数と同じ配列番号のクイズが出題されたかチェック
 - 出題されていたら乱数の生成しなおし
 - 出題されていなかったら出題する
 
プログラム全文
今回作成したプログラムのご紹介です。
のちほど、プログラムの解説を行います。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define	QUIZ_NUM	3
#define	FLAG_ON		1
#define	FLAG_OFF	0
typedef struct {
	char	mondai[100];
	char	sentaku[3][100];
	int		answer_no;
	int		done_flag;
}quiz_t;
void set_quiz(quiz_t* t, char* m, char* s1, char* s2, char* s3, int a)
{
	strncpy(t->mondai, m, sizeof(t->mondai));
	strncpy(t->sentaku[0], s1, sizeof(t->sentaku[0]));
	strncpy(t->sentaku[1], s2, sizeof(t->sentaku[1]));
	strncpy(t->sentaku[2], s3, sizeof(t->sentaku[2]));
	t->answer_no = a;
	t->done_flag = FLAG_OFF;
}
int main(void)
{
	int		ans;
	char	result[QUIZ_NUM][10];
	int		ok_cnt = 0;
	int		i;
	quiz_t	quiz[QUIZ_NUM];
	int		r;
	srand((unsigned int)time(NULL));
	set_quiz(&quiz[0], "リンゴは英語で何と言う?", "apple", "orange", "banana", 1);
	set_quiz(&quiz[1], "大正->昭和->○○->令和\n○○に入る年号は?", "慶応", "明治", "平成", 3);
	set_quiz(&quiz[2], "世界三大珍味はどれ?", "イカスミ", "キャビア", "チーズ", 2);
	i = 0;
	while (i < QUIZ_NUM) {
		r = rand() % QUIZ_NUM;
		if (quiz[r].done_flag == FLAG_ON) {
			continue;
		}
		printf("[第%d問]\n", i + 1);
		printf("%s\n", quiz[r].mondai);
		printf("1:%s  2:%s  3:%s\n", quiz[r].sentaku[0], quiz[r].sentaku[1], quiz[r].sentaku[2]);
		printf(">>> ");
		scanf("%d", &ans);
		if (ans == quiz[r].answer_no) {
			printf("正解!\n\n");
			strncpy(result[i], "○", sizeof(result[i]));
			ok_cnt++;
		}
		else {
			printf("不正解...\n");
			printf("正解は %d:%s です。\n\n", quiz[r].answer_no, quiz[r].sentaku[quiz[r].answer_no - 1]);
			strncpy(result[i], "×", sizeof(result[i]));
		}
		quiz[r].done_flag = FLAG_ON;
		i++;
	}
	printf("--クイズ終了--\n");
	for (i = 0; i < QUIZ_NUM; i++) {
		printf("[第%d問]...%s\n", i + 1, result[i]);
	}
	printf("あなたは %d 問中 %d 問正解でした (正答率:%d %%)\n", QUIZ_NUM, ok_cnt, (ok_cnt * 100) / QUIZ_NUM);
	return 0;
}プログラムの解説
ヘッダーの追加(乱数生成用)
#include <stdlib.h>
#include <time.h>「stdlib.h」「time.h」をインクルードしています。
これらは乱数を生成するために必要となります。
フラグ制御用定数を追加
#define	FLAG_ON		1
#define	FLAG_OFF	0対象のクイズが出題されたかどうかをチェックするためにフラグ制御を行います。
フラグ値としてON(1)、OFF(0)を定義しておきます。
構造体にフラグ追加
typedef struct {
	// 割愛
	int		done_flag;
}quiz_t;構造体メンバーに『このクイズは出題済みか』をチェックするためのフラグを追加します。
フラグ値は以下の2パターンです。
| フラグ値 | 意味 | 
|---|---|
| FLAG_ON(1) | 出題済み | 
| FLAG_OFF(0) | 未出題 | 
フラグ初期化処理の追加
void set_quiz(quiz_t* t, char* m, char* s1, char* s2, char* s3, int a)
{
	// 割愛
	t->done_flag = FLAG_OFF;
}追加したフラグ(done_flag)を初期化する処理を追加します。
すべてのクイズは関数「set_quiz」を呼んで登録するようになっているので、この関数内で初期化すればラクチンです!
一律『FLAG_OFF(未出題)』に初期化します。
乱数の生成
	int		r;クイズをランダム出題するために乱数を使用します。
乱数を保持するための変数rを宣言します。
	srand((unsigned int)time(NULL));乱数を初期化するための処理を追加します。
この一文がないと、初回のビルド時は乱数が生成されますが、次回以降は初回と同じ値が生成されてしまいます。
プログラム実行毎に乱数を生成するために、乱数の初期化処理を行います。
上記は『現在時刻で乱数を初期化する』という意味です。
		r = rand() % QUIZ_NUM;rand関数で乱数を生成します。
rand関数はランダムな数値を返却します。
「r = rand();」これだけだとちょっと使い勝手が悪いのですが、任意の値で割った余り(剰余)を求めることで使い勝手が大幅にアップします。
剰余では、必ず「0~指定した任意の値-1」の値が求められます。
5で割った余りは0~4のいずれかに、100で割った余りは0~99のいずれかになるといった感じです。
今回、クイズは全部で3問あります。
3で割った余りを求めれば返却される値は0,1,2のいずれかとなりますので、どのクイズを出題するかランダムに選択できるというわけです。
		printf("%s\n", quiz[r].mondai);		printf("1:%s  2:%s  3:%s\n", quiz[r].sentaku[0], quiz[r].sentaku[1], quiz[r].sentaku[2]);		if (ans == quiz[r].answer_no) {			printf("正解は %d:%s です。\n\n", quiz[r].answer_no, quiz[r].sentaku[quiz[r].answer_no - 1]);生成した乱数に対応するクイズを出題するように変更します。
具体的には、quiz[i]としていたところをquiz[r]に変更しています。
出題済みかチェック
		if (quiz[r].done_flag == FLAG_ON) {
			continue;
		}構造体メンバーの「done_flag」の値をチェックし、出題済み(FLAG_ON)だった場合は後続のループ処理をスキップ(continue)します。
continueは、強制的に次のループ処理を行うというもの。
continue以降の処理は実行されず、ループの先頭に戻ります。
	while (i < QUIZ_NUM) {
		// 割愛
		i++;
	}forループを使用していると、continueした際にforの3つ目の要素(i++)が実行されてしまいます。
このままだと、クイズの出題をcontinueでスキップしたにも関わらずループカウンターが進むため、出題数に影響してしまいます。
「第1問→第2問→第3問」としたいのに、「第1問→第3問」となってしまうおそれがあります。
今回の場合、continue後はiの値を変えたくないので、ループの種類をwhileループに変更しました。
while文ではループカウンターiはカウントアップされないため、明示的に「i++;」の処理を追加しています。
	i = 0;
	while (i < QUIZ_NUM) {
		// 割愛
	}変更に伴い、変数iの初期化処理を追加しています。
クイズ出題済みフラグを立てる
		quiz[r].done_flag = FLAG_ON;クイズを出題したら、そのクイズの出題済みフラグをONにします。
こうすることで、次回以降このクイズが出題されることはありません。
次回



