MENU

【C言語】クイズゲーム作成(7)問題をファイルから読み込む

クイズゲーム作成(7)問題をファイルから読み込む

こんにちは、コン(@pippi_kon)です。

この記事では『C言語でのクイズゲームの作り方』をご紹介しています。

前回は、プログラムを実行するたびに問題をランダムで出題する方法をご紹介しました。

出題にランダム性を取り入れることでクイズっぽくなりましたね!

でも、現状のプログラムだとランダムといってもまだ3問しかありません。

よりクイズっぽくするためには、もっと問題を増やしたいですよね。

ただ、単純に問題を増やすだけでは、プログラムの中がクイズの問題だらけになってしまいます。

そこで今回は、クイズの数を増やす前にクイズの問題を外部ファイル(テキストファイル)から読み込む方法をご紹介します。

OS:Windows 10(x64)
開発環境:Visual Studio 2019(Community)

目次

今回の目標

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

クイズゲーム07_今回の目標

出力結果自体は前回と変わりませんが、中身のプログラム処理が変わります

今回、クイズ問題が書かれた外部ファイルを読み込む処理を行っています。

ファイル読み込み処理に失敗した場合、エラーメッセージを出力するようにしています。

メッセージの内容はプログラムの解説と合わせて行います。

▲目次へ戻る

クイズ問題ファイルの記載形式

クイズ問題ファイルには以下の内容をリストアップします。

  • 問題文
  • 選択肢1~3
  • 正解の番号

ただし、適当にリストアップするのではダメです。

ファイルの内容をプログラム内で解析する必要があるので、記載方法をルール化しなければいけません。

今回は以下のように「1問ずつカンマ(,)区切りで記載」をルールとしました。

1問目の問題文,選択肢1,選択肢2,選択肢3,正解の番号
2問目の問題文,選択肢1,選択肢2,選択肢3,正解の番号
3問目の問題文,選択肢1,選択肢2,選択肢3,正解の番号

実際のクイズ問題ファイルの中身は以下の通り。

リンゴは英語で何と言う?,apple,orange,banana,1
大正->昭和->○○->令和\n○○に入る年号は?,慶応,明治,平成,3
世界三大珍味はどれ?,イカスミ,キャビア,チーズ,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
#define	QUESTION_FILE	"questions.txt"

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;
}

void change_kaigyou(char* s)
{
	char* p;
	char* w;

	while (1) {
		p = strstr(s, "\\n");
		if (p == NULL) {
			return;
		}
		*p = '\n';
		*p++;
		w = p;
		*w++;
		while (*p++ = *w++);
	}
}

int main(void)
{
	int		ans;
	char	result[QUIZ_NUM][10];
	int		ok_cnt = 0;
	int		i;
	quiz_t	quiz[QUIZ_NUM];
	int		r;
	FILE* fp;
	int		set_quiz_error_flag = FLAG_OFF;
	char	line[1000];
	char* ptr_mondai;
	char* ptr_sentaku1;
	char* ptr_sentaku2;
	char* ptr_sentaku3;
	char* ptr_answer_no;

	fp = fopen(QUESTION_FILE, "r");
	if (fp == NULL) {
		printf("問題リストのオープンに失敗しました。ファイル名 = %s\n", QUESTION_FILE);
		return -1;
	}

	i = 0;
	while (fgets(line, sizeof(line), fp) != NULL) {
		change_kaigyou(line);
		ptr_mondai = strtok(line, ",");
		if (ptr_mondai == NULL) {
			printf("問題文の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku1 = strtok(NULL, ",");
		if (ptr_sentaku1 == NULL) {
			printf("選択肢1の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku2 = strtok(NULL, ",");
		if (ptr_sentaku2 == NULL) {
			printf("選択肢2の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku3 = strtok(NULL, ",");
		if (ptr_sentaku3 == NULL) {
			printf("選択肢3の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_answer_no = strtok(NULL, ",");
		if (ptr_answer_no == NULL) {
			printf("正解の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		set_quiz(&quiz[i], ptr_mondai, ptr_sentaku1, ptr_sentaku2, ptr_sentaku3, atoi(ptr_answer_no));
		i++;
	}

	fclose(fp);
	if (set_quiz_error_flag == FLAG_ON) {
		return -1;
	}

	srand((unsigned int)time(NULL));

	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;
}

▲目次へ戻る

スポンサーリンク

プログラムの解説

外部ファイル名用定数を追加

#define	QUESTION_FILE	"questions.txt"

クイズの問題を記載してあるファイル名を定数で宣言します。

今回は「questions.txt」という名前を指定しています。

実際にこの名前でファイルを作り、プロジェクト関連のファイルがある場所に保存しておいてください。

通常だと、「.vcxproj」という拡張子のファイルや、ソースファイルがある場所です。

よくわからない場合や、別の場所にファイルを置きたい場合は、そのファイルパスを定数に指定すればOKです。

例えば、「D:\mydir\Program\quiz」のフォルダの中に「mondai.txt」というファイルを作りたい場合は、定数に『D:\\mydir\\Program\\quiz\\mondai.txt』と指定します。

円マーク(\)は2つずつにすること。1つだとエスケープ文字として認識されてしまいます。

▲目次へ戻る

クイズ問題ファイルのオープン

外部ファイルを読み込む大まかな流れは以下の通り。

  1. ファイルのオープン
  2. ファイルの読み込み
  3. ファイルのクローズ

まずはファイルオープン処理についてです。

	fp = fopen(QUESTION_FILE, "r");

ファイルのオープンは「fopen関数」で行います。

引数は2つあります。意味は以下の通り。

引数意味
第一引数オープンするファイルパス
第二引数オープンするモード
↓よく使うモード
"r":読み込み専用
"w":書き込み専用
"a":追加書き込み専用

今回は、第一引数に定数「QUESTION_FILE」を、第二引数に読み込み専用である"r"を指定します。

fopen関数の発行に成功(ファイルが開けた)したら、ファイルポインタがリターンされます。

以降のファイル操作系関数では、このファイルポインタを用いて制御を行いますので、変数fpに保存しておきます。

	if (fp == NULL) {
		printf("問題リストのオープンに失敗しました。ファイル名 = %s\n", QUESTION_FILE);
		return -1;
	}

fopen関数の発行が失敗したら、リターン値はNULLとなります。

ファイルのオープンに失敗したときのためにエラー処理を行っておきます。

失敗した旨のメッセージとともに、fopen関数に指定したファイル(開けなかったファイル名)も表示しておくと、エラーとなったときに調査しやすいです。

エラー後は後続の処理をしても無駄なので、おとなしくreturnして処理を終了させます。

今回の場合はあまり意味ないですが、プログラムを外部実行したときのために異常値を示す「-1」でリターンしておきます。

▲目次へ戻る

スポンサーリンク

ファイルの読み込みと解析

	i = 0;
	while (fgets(line, sizeof(line), fp) != NULL) {
		change_kaigyou(line);
		ptr_mondai = strtok(line, ",");
		if (ptr_mondai == NULL) {
			printf("問題文の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku1 = strtok(NULL, ",");
		if (ptr_sentaku1 == NULL) {
			printf("選択肢1の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku2 = strtok(NULL, ",");
		if (ptr_sentaku2 == NULL) {
			printf("選択肢2の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku3 = strtok(NULL, ",");
		if (ptr_sentaku3 == NULL) {
			printf("選択肢3の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_answer_no = strtok(NULL, ",");
		if (ptr_answer_no == NULL) {
			printf("正解の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		set_quiz(&quiz[i], ptr_mondai, ptr_sentaku1, ptr_sentaku2, ptr_sentaku3, atoi(ptr_answer_no));
		i++;
	}

ここが今回追加したメインの部分となります。

ここでやっていることは以下の通り

  1. ファイルから1行取得
  2. 取得した行を解析して問題文を取得
  3. 取得した行を解析して選択肢1を取得
  4. 取得した行を解析して選択肢2を取得
  5. 取得した行を解析して選択肢3を取得
  6. 取得した行を解析して正解の番号を取得
  7. クイズの構造体に追加
  8. 次の行へ

細かくみていきます。

ファイルから1行取得

	while (fgets(line, sizeof(line), fp) != NULL) {
		// 割愛
	}

この一文は「ファイルを1行ずつ読み込み続ける」ということを意味しています。

fgets関数はファイルを1行読み込みます。

読み込むデータがなくなったときはfgets関数のリターンがNULLとなります。

fgets関数の引数の意味は以下の通り。

引数意味
第一引数読み込んだデータを保存する変数
第二引数読み込むデータ量
第三引数読み込むファイルのファイルポインタ
(fopen関数で取得したもの)

今回は1000バイトの容量をもったlineという変数にデータを保存します。

読み込むデータ量はlineに保存できる最大数で、sizeof関数で指定しています。

改行文字の変換

		change_kaigyou(line);

テキストファイルから読み込むデータには「\n」が含まれているものがあります。

【例】
大正->昭和->○○->令和\n○○に入る年号は?,慶応,明治,平成,3

これをそのまま出力すると「\n」が文字列として認識されるため、改行処理が行われません。

そのため、文字列の「\n」を改行コードに変換する処理が必要です。

C言語の標準関数にはそのようなものはありませんので、「change_kaigyou()」という関数を自作しました。

void change_kaigyou(char* s)
{
	char* p;
	char* w;

	while (1) {
		p = strstr(s, "\\n");
		if (p == NULL) {
			return;
		}
		*p = '\n';
		*p++;
		w = p;
		*w++;
		while (*p++ = *w++);
	}
}

「\n」が含まれている文字列が格納された変数を引数に指定することで、関数にて”\n”を改行コードに変換してくれます。

引数の内容を直接操作するので、戻り値として受け取る必要はありません。

スポンサーリンク

問題文の取得

		ptr_mondai = strtok(line, ",");

読み込んだ行を解析して問題文を取得します。

strtok関数は、指定した文字列を指定した区切り文字で分解できます。

strtok関数の引数の意味は以下の通り。

引数意味
第一引数分解する文字列
NULL指定時は前回からの続き
第二引数読み込むデータ量

今回は、ファイルから読み込んだ行(line)を区切り文字(,)で分解します。

		if (ptr_mondai == NULL) {
			printf("問題文の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

strtok関数での分解に失敗した場合はNULLがリターンされますので、エラー処理も行っておきましょう。

  1. エラーメッセージの表示
  2. エラーフラグをON
  3. 解析処理のループから脱出

エラーメッセージには、問題文の解析に失敗した旨と、読み込んだファイル名・解析中の行番号を表示しておくと、エラー時に調査しやすいです。

set_quiz_error_flagは、後のファイルクローズ処理後にプログラムを中断させるためのフラグです。

選択肢の取得

		ptr_sentaku1 = strtok(NULL, ",");
		if (ptr_sentaku1 == NULL) {
			printf("選択肢1の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

問題文につづいて選択肢1を解析取得します。

strtok関数の第一引数に「NULL」を指定すると、前回の分解から継続して分解するということができます。

		ptr_sentaku2 = strtok(NULL, ",");
		if (ptr_sentaku2 == NULL) {
			printf("選択肢2の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_sentaku3 = strtok(NULL, ",");
		if (ptr_sentaku3 == NULL) {
			printf("選択肢3の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

		ptr_answer_no = strtok(NULL, ",");
		if (ptr_answer_no == NULL) {
			printf("正解の登録に失敗しました。ファイル名 = %s, 行数 = %d\n", QUESTION_FILE, i + 1);
			set_quiz_error_flag = FLAG_ON;
			break;
		}

選択肢1と同様に、残りの選択肢2,3と正解の番号も分解します。

正解の番号の分解では、番号の最後にカンマがないですが、行の終端までの区切りとなるので問題ありません。

クイズの構造体に登録

		set_quiz(&quiz[i], ptr_mondai, ptr_sentaku1, ptr_sentaku2, ptr_sentaku3, atoi(ptr_answer_no));
		i++;

問題文・選択肢・正解の番号の解析が正常に終わったら、読み込んだ内容をクイズの構造体に登録します。

正解の番号は整数型で登録する必要があるので、以前も登場したatoi関数で整数型に変換して登録します。

登録後、カウンタiを進めておきましょう。

▲目次へ戻る

クイズ問題ファイルのクローズ

 	fclose(fp);

ファイルの読み込み処理が終わったので、オープンしていたファイルをクローズします。

開けたら閉じる。

▲目次へ戻る

ファイル読み込み失敗時はプログラム終了

	if (set_quiz_error_flag == FLAG_ON) {
		return -1;
	}

ファイル読み込みと解析処理で何かしら失敗していたら、後続の処理は行わずにプログラムを終了させます。

▲目次へ戻る

スポンサーリンク

次回

▲目次へ戻る

created by Rinker
¥2,750 (2023/02/04 10:34:35時点 Amazon調べ-詳細)
created by Rinker
¥2,530 (2023/02/04 10:34:36時点 Amazon調べ-詳細)
  • URLをコピーしました!